Система тестирования задач IOI
Version 0.6
Содержание
Введение
Как это работает
Библиотека TesterLib
Библиотека CheckLib
Шаблон программы Grader
Введение
Задачи IOI (во всяком случае, начиная с 2010) по процессу тестирования несколько отличаются от обычно создаваемых на DL (что автоматически означает необходимость выполнять весь процесс от начала до конца самостоятельно).
Собственно, процесс выполняется следующим образом: участник присылает библиотеку с решением (или архив с несколькими), которая подключается при компиляции исходника вспомогательной программы. После чего система подсовывает ей по очереди тесты из одной группы и читает результат. После обработки всех тестов группы считается конечный балл (который, само собой, может быть меньше, чем количество тестов в группе).
Чтобы устанавливать эти задачи на DL, я разработал более-менее работоспособную замену системы тестирования. Здесь описан процесс установки задачи с её использованием.
Библиотеки TesterLib, CheckerLib и код задачи Parrots (IOI 2011 d2 t3) можно скачать одним архивом.
Как это работает
Итак, что происходит при запуске тестирования?
- Delta открывает файл task.cfg и видит там текст следующего содержания:
TYPE = USERS
CHECKSUBJECT = FILE
CHECKFILES = {*.*}
CHECKER = 'tester.exe'
- В соответствии с текстом, Delta копирует все файлы в корне задачи в корень папки тестирования и запускает tester.exe, ожидая, что тот закончит работу не позднее чем через минуту
- Тестер производит тестирование
- Delta читает файл результата и выводит результат как за один тест.
Тестер выполняет задачи, которые по-хорошему должна была выполнять сама Delta:
- Первым делом он читает свой файл конфигурации
- Далее он запускает скрипт подготовки, выполняющий компиляцию решения и т.п.
- Переименовывая по очереди входные файлы тестов в нужное название, тестер запускает программу checker.exe, которая отвечает за тестирование одного теста и выдачу результата этого тестирования.
- Собрав данные по всем тестам группы, тестер вычисляет балл за группу тестов (если какой-то тест в группе был не пройден, последующие уже не проверяются)
- В конце тестер выводит информацию по тестам в файл результата
Скрипт подготовки обычно называется prepare.bat и выполняет следующие действия:
- Даёт файлам исходников правильные названия (они переименованы в малополезной попытке получения хоть какой-то защиты от обмана)
- Запускает один из скриптов компиляции Дельты
Чекер выполняет следующие действия:
- Запускает скрипт limiter.bat (передающий работу лимитеру Дельты) с полученными ограничениями по времени/памяти для программы-оценщика (grader.exe)
- Если программа завершилась некорректно или была остановлена лимитером (или ничего не вывела), чекер замещает её файл вывода своим (где "всё плохо")
Оценщик компилируется в начале тестирования (решение - одна из его библиотек). Он:
- Считывает входной файл и готовит переменные окружения
- Запускает функцию (или функции) решения, как описано в условии
- Контролирует результаты выполнения функции, проверяет результаты её выполнения (насколько это позволяют данные входного файла)
Библиотека TesterLib (Free Pascal)
Для написания тестера достаточно написать несколько специфических (для задачи) функций/процедур (если они нужны), всё остальное реализовано в библиотеке TesterLib.
Конфигурация
Тестер при запуске считывает файл tester.cfg, который обрабатывается следующим образом:
- Строка начиная с первого символа "#" и до конца игнорируется (комментарии)
- Пустые строки (содержащие только пробелы и/или комментарий) игнорируются
- Если строка начинается с символа "<", начинается режим чтения групп тестов (после символа может быть указано их количество)
- В противном случае строка обрабатывается как опция (в формате "название=значение")
В режиме чтения групп тестов (игнорирование по тем же правилам):
- Если строка начинается с символа ">", режим чтения групп заканчивается.
- Строка разбивается на части, разделяемые запятой (",")
- Первая часть - число баллов за тест
- Вторая (если есть и не пуста) - число тестов в группе (иначе 0)
- Третья - дополнительная пометка (для вычисления количества баллов; тж. см. FRate в подразделе "Библиотека")
Поддерживаются следующие опции:
- InFile - входной файл (по умолчанию "grader.in")
- OutFile - выходной файл (по умолчанию "grader.out")
- ResFile - файл результата (по умолчанию "$result$.txt")
- PreRun - скрипт подготовки (указывать всегда, т.к. по умолчанию не выполняется)
- MemLimit - ограничение по памяти (можно указывать в байтах или добавлять "B", "KB", "MB", "GB"; по умолчанию "256MB")
- TimeLimit - ограничение по времени (в секундах; по умолчанию "2")
- Limiter - скрипт вызова лимитера (по умолчанию "limiter.bat")
Библиотека
Для использования тестера нужно подключить библиотеку в начале файла:
В теле тестера нужно вызвать две процедуры:
BEGIN
Body();
POut();
END.
Эти процедуры имеют несколько параметров-указателей со значениями по умолчанию (все NIL). Для передачи указателя нужно указать имя переменной (или процедуры/функции) с собакой перед ним: @Data.
Пример тестера.
Body()
Body (@Data, @Check, @Rate, @Reset, @Print);
- Data = Pointer
Структура с данными (любая) для хранения данных по группе тестов
- Check = Function (Data : Pointer; Chk : PText): Boolean;
Функция проверки теста; по умолчанию сравнивает первую строку со строковой константой Approval.
- Rate = Function (Max : LongWord; Var Failed : Boolean; Remark : AnsiString; Data : Pointer): LongWord;
Функция оценки группы тестов; по умолчанию ставит Max за пройденную группу и 0 за непройденную.
- Reset = Procedure (Tests : LongWord; Remark : AnsiString; Data : Pointer);
Процедура инициализации Data перед новой группой тестов; по умолчанию заполняет нулями по размеру (НЕ использовать с полями AnsiString!).
- Print = Procedure (Data : Pointer)
Делает отладочный вывод; по умолчанию – ничего.
В Body() происходит следующее:
- Перебираются тесты (именованные в формате DL, т.е. "${номер}.in" и "${номер}.out") и группы тестов (группа 1 содержит первые Tests[1] тестов, группа 2 содержит следующие Tests[2] тестов, и т.д.). Здесь и далее: i – номер группы, j – номер теста в группе, k – глобальный номер теста.
- В начале обработки группы вызывается процедура Reset(Tests[i], Remark[i], Data), которая инициализирует Data^ для обработки новой группы (по умолчанию выполняется FillChar(Data^, SizeOf(Data^), $00)).
- Failed[i] (пометка о проваленном тесте) устанавливается в False.
- Отладочный вывод: в stdout выводится (i + "[" + Tests[i] + "]")
- В начале обработки теста выводится "+"
- Если установлен Failed[i], тест пропускается
- Файл k + ".in" переименовывается в InFile
- Вызывается чекер с параметрами Limiter, MemLimit и TimeLimit
- InFile возвращается на место
- Если файл k + ".out" существует, он открывается для чтения в Chk и адрес сохраняется в PChk, иначе в PChk сохраняется NIL
- Открывается OutFile и вызывается Check(Data, PChk), которая читает из Input выходной файл (а из PChk^ проверочный, если там не NIL), обновляет Data^ и возвращает новое значение Failed[i].
- Файлы закрываются
- После обработки всех тестов в группе балл за группу (Res[i]) вычисляется с помощью Rate(Max[i], Failed[i], Remark[i], Data); также она может обновить Failed[i] на случай, если группа имеет специфическую проверку, не реализованную в Check() (по умолчанию Rate() просто проверяет текущее значение Failed[i]).
- Отладочный вывод: выполняется Print(Data), после чего выводится (" " + Res[i]).
- Res[i] прибавляется к суммарному баллу.
POut()
Procedure POut (@IsPartial);
- IsPartial = Function (Remark : AnsiString): Boolean;
Проверяет тест на наличие частичного балла (для вывода в виде Ball/Max); по умолчанию возвращает False.
В POut() происходит следующее:
- Открывается ResFile, в первую строку выводится суммарный балл
- Во вторую выводится информация по группам через запятую:
- "-" для проваленных (Failed[i])
- "+" для полностью пройденных тестов
- "*" для частично пройденных тестов
- Также, для пройденных тестов без частичной разбалловки в скобках выводится балл; для непроваленных тестов с частичным баллом - "балл/максимум". Считается ли тест частичным, определяет функция IsPartial(Remark[i]) (вариант по умолчанию всегда возвращает False).
Data
Data – указатель на структуру данных, сохраняющих данные по тестам (с помощью которых производится вычисление частичных баллов). Он может ссылаться на переменную любого типа. Удобнее всего делать его записью (Record – группа переменных):
TYPE
TData = Record
Sum, R : LongWord;
Q : Extended;
End;
VAR
Data : TData;
При работе с полями параметра Data удобно работать в блоке with:
Procedure ...
Begin
With (TData(Data^)) Do Begin
Inc(R);
Q:=Sum / R;
End;
End;
Если вычислять частичные баллы не требуется, обычно можно обойтись без сохранения промежуточных данных (исключение – если группа тестов имеет дополнительное ограничение, которое не определяется grader'ом). В этом случае можно вызвать Body() с параметрами по умолчанию, или передать первым параметром NIL (если нужно передать ссылку на функцию/процедуру).
Check
Function Check (Data : Pointer; Chk : PText): Boolean;
Функция Check получает в качестве параметров два указателя: Data (переданный первым параметром в Body) и Chk (указатель на открытый для чтения файл проверочных данных теста – {номер_теста}.out; если этого файла нет, указатель содержит NIL); в Input открыт выходной файл grader'а. После прочтения данных из этих файлов и сохранения нужной информации в Data^, функция возвращает True (если тест был пройден) или False (в противном случае).
Первая строка выходного файла пройденного теста содержит строку, равную константе Approval. Последующие строки содержат данные, заносимые в Data^ (например, количество запросов – в Data^ сохраняется максимум как худшее значение).
Также в этой функции выполняются проверки, для которых недостаточно знать входные данные (эти проверки выполняет grader). Нужные данные получаются из файла Chk^.
Rate
Function Rate (Max : LongWord; Var Failed : Boolean; Remark : AnsiString; Data : Pointer): LongWord;
Функция Rate получает в качестве параметров Max (максимальный балл за группу тестов), Failed (содержит True если все тесты в группе были пройдены), Remark (пометка – третья часть описания группы тестов) и указатель Data. Она возвращает итоговый балл за группу; если группа по какой-то причине не пройдена, значение Failed устанавливается в False.
Обычно функция возвращает Max (если Failed = True) или 0 (в противном случае). Необходимость другого поведения (дополнительная проверка или частичный балл) определяется пометкой, а для его обработки используется Data^. Если группа имеет одно или несколько дополнительных ограничений, их можно описать в пометке как числа (через пробел), а для прочтения использовать процедуру ReadStr (она работает так же, как и Read, но первым параметром принимает строку, из которой читаются данные); в простых случаях достаточно помещать в пометку один символ.
Reset
Procedure Reset (Tests : LongWord; Remark : AnsiString; Data : Pointer);
Процедура Reset получает в качестве параметров Tests (количество тестов в группе), Remark (пометка) и указатель Data. Она инициализирует структуру Date^ для работы с новой группой тестов.
Обычно процедура заполняет Data^ нулевым байтом (если Data – не NIL). Однако это не всегда хорошая идея (скажем, если Data^ содержит поля типа AnsiString или другие указатели). Также, если для работы с группой требуется заранее иметь информацию о ней, можно использовать Tests и Remark.
Print
Procedure Print (Data : Pointer);
Процедура Print получает в качестве параметра указатель Data. Она делает отладочный вывод в консоль (эта информация может быть полезна при запуске тестера вручную). Вывод выполняется после вычисления баллов за группу. В качестве источника данных используется структура, на которую указывает Data.
Обычно Print ничего не делает.
IsPartial
Function IsPartial (Remark : AnsiString): Boolean;
Функция IsPartial получает в качестве параметра Remark (пометку). Она определяет, может ли группа получить частичный балл (чтобы полностью пройденные группы тестов с частичными баллами отображались в формате Ball/Max).
Обычно IsPartial возвращает False.
Библиотека CheckLib
Для написания чекера достаточно использовать библиотеку CheckLib и написать несколько строк кода.
Чекер получает 3 параметра: название скрипта-лимитера и его параметры-ограничения (по памяти и по времени). Обработка этих параметров (как большая часть остального функционала) выполняется библиотекой. Всё, что нужно сделать в теле – вызвать функцию запуска для каждого grader'а и переместить промежуточные файлы.
Если grader называется gradera, он будет читать данные из файла gradera.in и выводить данные в gradera.out. Соответственно, если после этого запускается grader, который называется graderb, нужно переместить gradera.out в graderb.in. В файле tester.cfg входной файл будет называться gradera.in, а выходной – graderb.out. Для удобства, используется константа Grader.
Таким образом, тело этого чекера будет выглядеть следующим образом:
Uses
CheckLib, SysUtils;
CONST
Gradera = Grader+'a';
Graderb = Grader+'b';
BEGIN
Run(Gradera, Graderb+'.out');
DeleteFile(Graderb+'.in');
RenameFile(Gradera+'.out', Graderb+'.in');
Run(Graderb, Graderb+'.out');
END.
Процедура Run получает в качестве параметров две строки: название grader'а и название (последнего) выходного файла. Она запускает grader с помощью лимитера, проверяет вывод и, если grader не создал выходной файл (или если лимитер прервал его работу), создаёт выходной файл с соответствующей пометкой в первой строке и прерывает работу чекера.
Шаблон программы Grader
Как было упомянуто выше, grader с названием gradera читает данные из файла gradera.in и выводит результат в gradera.out. Первая строка вывода содержит Approval ('OK') для пройденного теста (или любую другую строку для непройденного), последующие – информацию для вычисления частичного балла (или пост-проверки).
Обычно чтение выносится в процедуру ReadAll, вывод – в процедуру WriteAll. В теле программы между их вызовами выполняется инициализация вспомогательной библиотеки (если таковая имеется), вызовы функций/процедур решения и проверка их работы.
Таким образом, тело программы может выглядеть приблизительно так:
Uses
CONST
InFile = 'gradera.in';
OutFile = 'gradera.out';
CONST
Approval = 'OK';
VAR
Procedure ReadAll();
Var
Begin
Assign(Input, InFile);
Reset(Input);
Close(Input);
End;
Procedure WriteAll();
Var
Begin
Assign(Output, OutFile);
Reset(Output);
Close(Output);
End;
VAR
BEGIN
ReadAll();
WriteAll();
END.