Просмотр источника
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} |