h1. Система тестирования задач IOI


h2. _Version 0.8_

([История версий|Установка задач IOI - история версий])

h2. {anchor:содержание}Содержание

[Введение|#введение]
[Как это работает|#howitworks]
[Библиотека TesterLib|#testerlib]
[Библиотека CheckLib|#checklib]
[Шаблон программы Grader|#grader]

h2. {anchor:введение}Введение

Задачи IOI (во всяком случае, начиная с 2010) по процессу тестирования несколько отличаются от обычно создаваемых на DL (что автоматически означает необходимость выполнять весь процесс от начала до конца самостоятельно).

Собственно, процесс выполняется следующим образом: участник присылает _библиотеку_ с решением (или архив с несколькими), которая подключается при компиляции исходника вспомогательной программы. После чего система подсовывает ей по очереди тесты из одной группы и читает результат. После обработки всех тестов группы считается конечный балл (который, само собой, может быть меньше, чем количество тестов в группе).

Чтобы устанавливать эти задачи на DL, я разработал более-менее работоспособную замену системы тестирования. Здесь описан процесс установки задачи с её использованием.

Библиотеки TesterLib, CheckerLib и код задачи Parrots (IOI 2011 d2 t3) можно скачать [одним архивом|http://dl.gsu.by/images/agulenko/IOI_tester.rar].

h2. {anchor:howitworks}Как это работает

Итак, что происходит при запуске тестирования?
* Delta открывает файл {{[task.cfg|http://dl.gsu.by/doc/use/taskcfg.htm]}} и видит там текст следующего содержания:
{code:title=task.cfg}
TYPE = USERS
CHECKSUBJECT = FILE
CHECKFILES = {*.*}
CHECKER = 'tester.exe'
{code}

* В соответствии с текстом, Delta копирует все файлы в корне задачи в корень папки тестирования и запускает {{tester.exe}}, ожидая, что тот закончит работу не позднее чем через минуту
* Тестер производит тестирование
* Delta читает файл результата и выводит результат как за один тест.

Тестер выполняет задачи, которые по-хорошему должна была выполнять сама Delta:
* Первым делом он читает свой файл конфигурации
* Далее он запускает скрипт подготовки, выполняющий компиляцию решения и т.п.
* Переименовывая по очереди входные файлы тестов в нужное название, тестер запускает программу {{checker.exe}}, которая отвечает за тестирование одного теста и выдачу результата этого тестирования.
* Собрав данные по всем тестам группы, тестер вычисляет балл за группу тестов (если какой-то тест в группе был не пройден, последующие уже не проверяются)
* В конце тестер выводит информацию по тестам в файл результата

Скрипт подготовки обычно называется {{[prepare.bat|http://dl.gsu.by/images/agulenko/IOI11_Parrots/prepare.bat]}} и выполняет следующие действия:
* Даёт файлам исходников правильные названия (они переименованы в малополезной попытке получения хоть какой-то защиты от обмана)
* Запускает один из скриптов компиляции
*- Для получения информации об ошибках компиляции необходимо проверять/возвращать ErrorLevel (см. скрипт в примере).

Чекер выполняет следующие действия:
* Запускает скрипт {{[limiter.bat|http://dl.gsu.by/images/agulenko/IOI_common/limiter.bat]}} (передающий работу лимитеру Дельты) с полученными ограничениями по времени/памяти для программы-оценщика ({{grader.exe}})
* Если программа завершилась некорректно или была остановлена лимитером (или ничего не вывела), чекер замещает её файл вывода своим (где "всё плохо")

Оценщик компилируется в начале тестирования (решение - одна из его библиотек). Он:
* Считывает входной файл и готовит переменные окружения
* Запускает функцию (или функции) решения, как описано в условии
* Контролирует результаты выполнения функции, проверяет результаты её выполнения (насколько это позволяют данные входного файла)

h2. {anchor:testerlib}Библиотека TesterLib (Free Pascal)

Для написания тестера достаточно написать несколько специфических (для задачи) функций/процедур (если они нужны), всё остальное реализовано в библиотеке {{[TesterLib|http://dl.gsu.by/images/agulenko/IOI_common/TesterLib.pp]}}.

h3. {anchor:testerlib_cfg}Конфигурация

Тестер при запуске считывает файл {{tester.cfg}}, который обрабатывается следующим образом:
* Строка начиная с первого символа "{{#}}" и до конца игнорируется (комментарии)
* Пустые строки (содержащие только пробелы и/или комментарий) игнорируются
* Если строка начинается с символа "{{<}}", начинается режим чтения групп тестов (после символа может быть указано их количество)
* В противном случае строка обрабатывается как опция (в формате "название=значение")

В режиме чтения групп тестов (игнорирование по тем же правилам):
* Если строка начинается с символа "{{>}}", режим чтения групп заканчивается.
* Строка разбивается на части, разделяемые запятой ("{{,}}")
* Первая часть - число баллов за тест
* Вторая (если есть и не пуста) - число тестов в группе (иначе 0)
* Третья - дополнительная пометка (для вычисления количества баллов; тж. см. [Rate()|#testerlib_rate] в параметрах [Body()|#testerlib_body])

Поддерживаются следующие опции:
* InFile - входной файл (по умолчанию "{{grader.in}}")
* OutFile - выходной файл (по умолчанию "{{grader.out}}")
* ResFile - файл результата (по умолчанию "{{$result$.txt}}")
* PreRun - скрипт подготовки (указывать всегда, т.к. по умолчанию не выполняется; если выполнится с ненулевым кодом завершения, это будет обработано как ошибка компиляции решения)
* MemLimit - ограничение по памяти (можно указывать в байтах или добавлять "{{B}}", "{{KB}}", "{{MB}}", "{{GB}}"; по умолчанию "{{256MB}}")
* TimeLimit - ограничение по времени (можно указывать в милисекундах или добавлять "{{MS}}", "{{S}}"; по умолчанию "{{2s}}")
* Limiter - скрипт вызова лимитера (по умолчанию "{{limiter.bat}}")
* Comment - выводимые комментарии ("{{+}}" для прошедших тестов, "{{\-}}" для непрошедших, "{{\*}}" для частично прошедших; по умолчанию "{{\-\*}}")

h3. {anchor:testerlib_unit}Библиотека

Для использования тестера нужно подключить библиотеку в начале файла:
{code:title=tester.pas}
Uses
TesterLib;
// ...
{code}
В теле тестера нужно вызвать две процедуры:
{code:title=tester.pas}
// ...
BEGIN
Body();
POut();
END.
{code}
Эти процедуры имеют несколько параметров-указателей со значениями по умолчанию (все {{NIL}}). Для передачи указателя нужно указать имя переменной (или процедуры/функции) с собакой перед ним: {{@Data}}.
[Пример тестера.|http://dl.gsu.by/images/agulenko/IOI11_Parrots/tester.pas]

h3. {anchor:testerlib_body}Body()

{code:title=tester.pas}
Body (@Data, @Check, @Rate, @Reset, @Print);
{code}
* {{[Data|#testerlib_data] = Pointer}}
Структура с данными (любая) для хранения данных по группе тестов
* {{[Check|#testerlib_check] = Function (Data : Pointer; Var Comment : AnsiString; Chk : PText): Boolean;}}
Функция проверки теста; по умолчанию сравнивает первую строку со строковой константой {{Approval}} и сохраняет её значение как комментарий.
* {{[Rate|#testerlib_rate] = Function (Max : LongWord; Var Failed : Boolean; Remark : AnsiString; Var Comment : AnsiString; Data : Pointer): LongWord;}}
Функция оценки группы тестов; по умолчанию ставит Max за пройденную группу и 0 за непройденную.
* {{[Reset|#testerlib_reset] = Procedure (Tests : LongWord; Remark : AnsiString; Data : Pointer);}}
Процедура инициализации {{Data}} перед новой группой тестов; по умолчанию заполняет нулями по размеру (*НЕ* использовать с полями {{AnsiString}}\!).
* {{[Print|#testerlib_print] = Procedure (Data : Pointer; Remark : AnsiString)}}
Делает отладочный вывод; по умолчанию -- ничего.

В {{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\])}} и (для соответствующих групп тестов, см. [Конфигурация|#testerlib_cfg]) комментарий.
* {{Res\[i\]}} прибавляется к суммарному баллу.

h3. {anchor:testerlib_pout}POut()

{code:title=tester.pas}
Procedure POut (@IsPartial);
{code}
* {{[IsPartial|#testerlib_ispartial] = Function (Remark : AnsiString): Boolean;}}
Проверяет тест на наличие частичного балла (для вывода в виде Ball/Max); по умолчанию возвращает {{False}}.

В {{POut()}} происходит следующее:
* Открывается {{ResFile}}, в первую строку выводится суммарный балл
* Во вторую выводится информация по группам через запятую:
* {{"-"}} для проваленных ({{Failed\[i\]}})
* {{"+"}} для полностью пройденных тестов
* {{"*"}} для частично пройденных тестов
* Также, для пройденных тестов без частичной разбалловки в скобках выводится балл; для непроваленных тестов с частичным баллом - "балл/максимум". Считается ли тест частичным, определяет функция {{IsPartial(Remark\[i\])}} (вариант по умолчанию всегда возвращает {{False}}).
* Для соответствующих групп тестов (см. [Конфигурация|#testerlib_cfg]) выводится комментарий.

h3. {anchor:testerlib_data}Data

{code:title=tester.pas}
Data : Pointer;
{code}
{{Data}} -- указатель на структуру данных, сохраняющих данные по тестам (с помощью которых производится вычисление частичных баллов). Он может ссылаться на переменную любого типа. Удобнее всего делать его записью ({{Record}} -- группа переменных):
{code:title=tester.pas}
TYPE
TData = Record
Sum, R : LongWord;
Q : Extended;
End;

VAR
Data : TData;
{code}
При работе с полями параметра Data удобно работать в блоке {{with}}:
{code:title=tester.pas}
Procedure ...
Begin
With (TData(Data^)) Do Begin
Inc(R);
Q:=Sum / R;
End;
End;
{code}
Если вычислять частичные баллы не требуется, обычно можно обойтись без сохранения промежуточных данных (исключение -- если группа тестов имеет дополнительное ограничение, которое не определяется grader'ом). В этом случае можно вызвать {{Body()}} с параметрами по умолчанию, или передать первым параметром {{NIL}} (если нужно передать ссылку на функцию/процедуру).

h3. {anchor:testerlib_check}Check()

{code:title=tester.pas}
Function Check (Data : Pointer; Var Comment : AnsiString; Chk : PText): Boolean;
{code}
Функция {{Check}} получает в качестве параметров два указателя: {{Data}} (переданный первым параметром в {{Body}}) и {{Chk}} (указатель на открытый для чтения файл проверочных данных теста -- \{номер_теста\}.out; если этого файла нет, указатель содержит {{NIL}}); в {{Input}} открыт выходной файл grader'а. После прочтения данных из этих файлов и сохранения нужной информации в {{Data\^}}, функция возвращает {{True}} (если тест был пройден) или {{False}} (в противном случае). Также она может модифицировать комментарий группы теста ({{Comment}}) на своё усмотрение.

Первая строка выходного файла пройденного теста содержит строку, равную константе {{Approval}}. Последующие строки содержат данные, заносимые в {{Data\^}} (например, количество запросов -- в {{Data\^}} сохраняется максимум как худшее значение).
Также в этой функции выполняются проверки, для которых недостаточно знать входные данные (эти проверки выполняет grader). Нужные данные получаются из файла {{Chk\^}}.

h3. {anchor:testerlib_rate}Rate()

{code:title=tester.pas}
Function Rate (Max : LongWord; Var Failed : Boolean; Remark : AnsiString; Var Comment : AnsiString; Data : Pointer): LongWord;
{code}
Функция {{Rate}} получает в качестве параметров {{Max}} (максимальный балл за группу тестов), {{Failed}} (содержит {{True}} если все тесты в группе были пройдены), {{Remark}} (пометка -- третья часть описания группы тестов) и указатель {{Data}}. Она возвращает итоговый балл за группу; если группа по какой-то причине не пройдена, значение {{Failed}} устанавливается в {{False}}. Также можно изменить значение комментария ({{Comment}}).

Обычно функция возвращает {{Max}} (если {{Failed = True}}) или {{0}} (в противном случае). Необходимость другого поведения (дополнительная проверка или частичный балл) определяется пометкой, а для его обработки используется {{Data\^}}. Если группа имеет одно или несколько дополнительных ограничений, их можно описать в пометке как числа (через пробел), а для прочтения использовать процедуру {{ReadStr}} (она работает так же, как и {{Read}}, но первым параметром принимает строку, из которой читаются данные); в простых случаях достаточно помещать в пометку один символ.

h3. {anchor:testerlib_reset}Reset()

{code:title=tester.pas}
Procedure Reset (Tests : LongWord; Remark : AnsiString; Data : Pointer);
{code}
Процедура {{Reset}} получает в качестве параметров {{Tests}} (количество тестов в группе), {{Remark}} (пометка) и указатель {{Data}}. Она инициализирует структуру {{Date\^}} для работы с новой группой тестов.

Обычно процедура заполняет {{Data\^}} нулевым байтом (если {{Data}} -- не {{NIL}}). Однако это не всегда хорошая идея (скажем, если {{Data\^}} содержит поля типа {{AnsiString}} или другие указатели). Также, если для работы с группой требуется заранее иметь информацию о ней, можно использовать {{Tests}} и {{Remark}}.

h3. {anchor:testerlib_print}Print()

{code:title=tester.pas}
Procedure Print (Data : Pointer; Remark : AnsiString);
{code}
Процедура {{Print}} получает в качестве параметра указатель {{Data}} и пометку группы тестов ({{Remark}}). Она делает отладочный вывод в консоль (эта информация может быть полезна при запуске тестера вручную). Вывод выполняется после вычисления баллов за группу. В качестве источника данных используется структура, на которую указывает {{Data}}.

Обычно {{Print}} ничего не делает.

h3. {anchor:testerlib_ispartial}IsPartial()

{code:title=tester.pas}
Function IsPartial (Remark : AnsiString): Boolean;
{code}
Функция {{IsPartial}} получает в качестве параметра {{Remark}} (пометку). Она определяет, может ли группа получить частичный балл (чтобы полностью пройденные группы тестов с частичными баллами отображались в формате Ball/Max).

Обычно {{IsPartial}} возвращает {{False}}.

h2. {anchor:checklib}Библиотека CheckLib

Для написания чекера достаточно использовать библиотеку {{[CheckLib|http://dl.gsu.by/images/agulenko/IOI_common/CheckLib.pp]}} и написать несколько строк кода.

Чекер получает 3 параметра: название скрипта-лимитера и его параметры-ограничения (по памяти и по времени). Обработка этих параметров (как большая часть остального функционала) выполняется библиотекой. Всё, что нужно сделать в теле -- вызвать функцию запуска для каждого grader'а и переместить промежуточные файлы.

Если grader называется {{gradera}}, он будет читать данные из файла {{gradera.in}} и выводить данные в {{gradera.out}}. Соответственно, если после этого запускается grader, который называется {{graderb}}, нужно переместить {{gradera.out}} в {{graderb.in}}. В файле {{tester.cfg}} входной файл будет называться {{gradera.in}}, а выходной -- {{graderb.out}}. Для удобства, используется константа {{Grader}}.

Таким образом, тело этого чекера будет выглядеть следующим образом:
{code:title=checker.pas}
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.
{code}
Процедура {{Run()}} получает в качестве параметров две строки: название grader'а и название (последнего) выходного файла. Она запускает grader с помощью лимитера, проверяет вывод и, если grader не создал выходной файл (или если лимитер прервал его работу), создаёт выходной файл с соответствующей пометкой в первой строке и прерывает работу чекера.

h2. {anchor:grader}Шаблон программы Grader

Как было упомянуто выше, grader с названием {{gradera}} читает данные из файла {{gradera.in}} и выводит результат в {{gradera.out}}. Первая строка вывода содержит {{Approval}} ('OK') для пройденного теста (или любую другую строку для непройденного), последующие -- информацию для вычисления частичного балла (или пост-проверки).

Обычно чтение выносится в процедуру {{ReadAll}}, вывод -- в процедуру {{WriteAll}}. В теле программы между их вызовами выполняется инициализация вспомогательной библиотеки (если таковая имеется), вызовы функций/процедур решения и проверка их работы.

Таким образом, тело программы может выглядеть приблизительно так:
{code:title=gradera.pas}
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.
{code}