Библиотека testlib.h
Здесь пойдет речь о библиотеке testlib.h, которая была написана мной достаточно давно — году в 2005-ом. Незадолго до этого было объявлено об отказе на финале ACM-ICPC от использования Pascal, популярность набирал TopCoder (где так же нет Pascal/Delphi). Все это приводило к мыслям, что писать чекеры на Pascal вечно невозможно, да и не всегда достаточно удобно, да и кроссплатформенным этот вариант назвать трудно — по-моему далеко не все testlib.pas (которых несколько разновидностей) компилируются free pascal.
Про чекеры
Так как не все читатели являются авторами задач, то давайте проясним смысл этого слова. Чекером называется программа, которая читает входной файл (тест), вывод проверяемой программы, предполагаемый ответ и выводит вердикт относительно корректности вывода проверяемой программы. Обычно, бывают следующие вердикты: OK (ответ верен, представлен один из правильных ответов), WA (ответ неверен), PE (формат вывода не верен, я этот вердикт не люблю), FL (произошел epic fail — например, чекер выяснил, что решение участника вывело более оптимальный ответ, чем авторское решение). Конечно, при тестировании подготовленных задач не должен появляться FL, но о нем мы расскажем чуть позже.
Конечно, в большинстве задач не требуется "интеллектуальный" чекер, так как условие задачи однозначно определяет вывод участника. Более того, на многих соревнованиях по техническим причинам (TopCoder) или в силу традиций (большое количество ACM-ICPC regionals) это стало правилом. С другой стороны, даже при однозначном выводе могут быть тонкости — на сколько позволять участникам не соблюдать формат. Возможны следующие моменты (и не только они):
- вывод перевода строки или его отсутствие в конце последней строки файла;
- вывод лишних пробелов, особенно в задачах со всякими "Case: 12" или завершающий пробел в конце последовательности чисел;
- вывод вещественных чисел — вообще, отдельная песня, так как надо определяться сколько нужно знаков (ровно столько? не меньше?), да и округлиться 0.34999999 может как угодно.
Все это приводит к заключению, что даже в задачах с однозначным выводом проверка вывода дело тонкое. Обычно, если в условии задачи не написано четко про пробелы, то следует допускать их произвольное расположение в выводе. Лояльность относительно заключительного перевода строки тоже, я думаю, правило хорошего тона. Так как в разных задачах оказывается, что требуется немного разное "точное" сравнение, то и в таких задачах применяют чекеры, просто их не пишут каждый раз, а используют готовые (написанные заранее).
Обычно, чекер компилируется в исполняемый файл, который принимает три (опционально два) аргумента командной строки: входной-файл, файл-вывода, файл-ответа. В англоязычной терминологии это input, output и answer. Возвращает значение чекер обычно через exit-codes выполняемого процесса. За исключением ejudge тестирующие системы обычно используют:
- код возврата 0 для обозначения OK;
- код возврата 1 для обозначения WA;
- код возврата 2 для обозначения PE;
- код возврата 3 для обозначения FL.
Что очень важно хороший чекер выведет в stdout вердикт и его причину в виде короткой фразы на английском языке (иногда для русскоязычных школьных соревнований используется русский язык). Стоит помнить, что вывод должен быть информативен и не очень громоздким. Примеры хороших выводов: "ok n=10, m=13, answer=34", "ok No solution", "wrong answer Expected -1, found 4294967295". Примеры плохих выводов "ok OK", "wrong answer Palevo!".
Чекеры и testlib.h
Писать чекеры — с одной стороны просто, с другой стороны в этом много подводных камней. Что может быть проще написать программу для сравнивания двух целых чисел? Однако вдруг оказывается, что scanf("%d", &a) (да и многие другие стандартные способы считать целое число во многих языках) при считывании 4294967295 не просигнализирует об ошибке, и будет уверен, что прочел -1. Отделить PE и WA при чтении целого числа тоже отдельная задачка.
По этой причине еще в лохматые 90-е Антоном Сухановым и Романом Елизаровым на был написан testlib для Pascal, который верой и правдой служил на многих NEERC и в модифицированном виде активно используется до сих пор. Иногда возникают проблемы обратной несовместимости версий этой библиотеки или около того (модуль symbols). Заметим, что эта библиотека была переписана Андреем Лопатиным и Николаем Дуровым и другой вариант активно используется на многих контестах СпбГУ и по сей день. На сколько я понимаю, библиотеки эти не совсем совместимы.
Библиотека testlib.h была написана мной в 2005-м году по образу и подобию тестлибов для Pascal. Это позволило упростить переход на нее тем многим, кто уже успел поработать с testlib.pas. Достаточно быстро testlib.h стал использоваться не только на саратовских соревнованиях. Одним из первых широкомасштабных использований стала Всероссийская олимпиада школьников года примерно 2006-го. С тех пор, testlib.h стал де-факто стандартом для написания чекеров на С++ и применяется на совсем разных контестах.
Приведем пример простейшего чекера на testlib.h, который сравнивает answer и output, ожидая в каждом из них по одному 32-х битному целому числу.
#include "testlib.h" int main(int argc, char * argv[]) { setName("compare two signed int32 numbers"); registerTestlibCmd(argc, argv); int ja = ans.readInt(); int pa = ouf.readInt(); if (ja != pa) quitf(_wa, "expected %d, found %d", ja, pa); quitf(_ok, "answer is %d", ja); }
Я думаю код достаточно понятен. Отмечу, что программе доступны три потока inf, ouf, ans — потоки входных, выходных данных и поток с ответом. Для потоков есть удобные функции чтения, которые позволяют читать разнообразные данные. Ошибки чтения трактуются по разному для разных потоков. Например, если невозможно прочитать число с помощью ouf.readInt(), то будет PE, но если использовался inf.readInt() или ans.readInt() то будет FL. Об этом не надо особенно задумываться при разработке — здесь все вполне логично и естественно. Еще немного примеров (подразумевается чтение из ouf, для inf и ans в случае ошибок будут FL).
- readInt(1, 100, "n") — попытается прочесть целое от 1 до 100, вернет WA (для версий 0.7+, иначе PE) если число не в заданных границах. Текст вывода будет примерно таким: Integer n violates the range [1, 100].
- readInt(1, 100) — тоже самое, но вывод об ошибке немного другой (без указания имени переменной).
- readLong() — читает знаковое 64-х битное число.
- readToken() — читает очередной токен (последовательность непробельных символов), пропускает если надо все whitespaces перед чтением токена.
- readWord() — тоже самое, что и readToken().
- readWord("[a-z]{1,100}") — читает токен и удостоверяется, что это последовательность малых латинских букв длины от 1 до 100, вернет WA (для версий 0.7+, иначе PE) если токен не соответствует паттерну.
- seekEof() — пропускает все whitespaces и возвращает true если больше ничего нет в потоке.
- readLine() — читает текущую строку до конца.
Это далеко не все возможности, следует посмотреть testlib.h (строки около 950) чтобы ознакомиться со списком всех возможностей.
Задачи с сертификатом, чекеры
Особо следует отметить такие задачи, в которых требуется сертификат. Так обычно называют вывод не только значения оптимального ответа (например длины наидлиннейшей возрастающей подпоследовательности), но и самой подпоследовательности (например, в виде индексов). Для таких задач существует определенный паттерн написания чекера. Основная часть чекера должна выглядеть как-то так:
n = inf.readInt(); int ja = readAnswer(ans, _fail, _fail); int pa = readAnswer(ouf, _wa, _pe); if (ja < pa) quitf(_wa, "Jury has better answer: %d < %d", ja, pa); if (ja > pa) quitf(_fail, "Participant has better answer: %d < %d", pa, ja); quitf(_ok, "n=%d, m=%d, result=%d", n, m, ja);
Здесь функция readAnswer читает ответ (вывод) из заданного потока, удостоверяется в его корректности и возвращает значение целевой функции оптимизации. Второй и третий параметры функции нужны для того, чтобы использовать их в функции quitf(), это обеспечит FL на ошибках потока ответа и WA/PE на ошибках потока вывода.
Как написать чекер
Приведу список шагов по написанию чекера.
- Если у вас нет неоднозначности в выводе в задаче, то используйте один из стандартных чекеров. Не бойтесь быть слишком лояльными — пусть лучше ваш чекер игнорирует какие-нибудь лишние пробелы, чем будет слишком строг и испортит участникам контест.
- Если в задаче предполагается вывод сертификата, то пишите чекер в соответствии с паттерном в предыдущем разделе. Всегда стоит рассмотреть возможность изменения задачи, чтобы она требовала вывод сертификата. Обычно это делает задачу чуть более программистской, что неплохо.
- Когда пишите чекер, то учтите, что он будет компилироваться на произвольной платформе. В частности, не используйте %I64d/%lld. В testlib.h есть функция vtos(value), которая возвращает строковое представление аргумента value.
- Старайтесь писать максимально надежно. Есть вероятность переполнения — пишите все в long long, и так далее.
- Старайтесь писать все просто и прямо. Не используйте хитрый и неочевидный код.
- Перепрочтите условие задачи и обратите внимание на то, что вы проверяете все требования, изложенные в задаче.
- Будьте аккуратны с WA/PE. Например, если в задаче требуют вывести число или "No solution", то вывод "No solution", не должен приводить к PE.
- Убедиться, что при любом выводе участника чекер не выведет сверхдлинный комментарий. Например, выводя строки их лучше оборачивать в __testlib_part(s). Стандартные чекеры делают именно так, можно почитать их код для уточнения деталей.
- Тщательно потестируйте ваш чекер на предмет его адекватной работы в каждом из возможных вариантов исхода.
Пожалуй все. Пишите в комментариях замечания и предложения. Наверняка, я что-то забыл/упустил.
MikeMirzayanov
P.S. Немного ссылок:
- Официальный сайт testlib.h
- Issue (bug) tracker для testlib.h
- testlib.pas by SPb IFMO CTD Development Team
У меня периодически гуляют мысли переписать тестлиб на питоне, но поскольку я проведением олимпиад напрямую не занимаюсь, то до дела это так и не дошло.
Интересно, кто-нибудь пытался сделать что-то подобное? Ведь, по-моему, питон отлично подходит как раз для чекеров. Я не говорю про задачи где чекер сложнее решения, но для остальных-то 90%.
Ну, эти 50% я вообще не считал)
На тему обратной совместимости я, честно говоря, не думал, но, в принципе, у питона с ней все не так и плохо. Внутри одной версии языка она в общем-то сохраняется, а новые выходят не так и часто и, например, 2 в 3 есть конвертер.
Хотя, у всего, конечно, есть свои минусы. Зато у него лучше обстоят дела с кроссплатформенностью чем у тех же паскаля и плюсов.
1.
Надо не забывать писать в конце чекера (перед тем как уже готовы вернуть _ok) проверку на то, чтобы в output ничего небыло. Иначе выдавать PE с соответствющим комментарием.Edit: testlib это делает сам
2. Я не знаю есть ли в testlib.h готовое решение для данного случая, в testlib.pas которым я пользовался небыло. Случай такой: мы хотим вывести комментарий типа "Expected %s, but found %s" со строками. Так вот надо быть аккуратным и обрезать строку, которую выдает участник, т.к. она может быть очень длинной.
3. Что будет если чекер упадет по run-time exception? Он как-то сам вернет 3 в качестве кода возврата? Или что угодно, но не ноль?
Второе _wa на самом деле _fail, в этом как раз основной пафос: если участник ВНЕЗАПНО нашёл ответ лучше, чем жюри, значит, где-то (в чекере, в решении, в генераторе?) у жюри точно есть баг. И жюри должно об этом как можно скорее узнать, а участник - получить не Wrong Answer, а Jury Error (хотя, например, testsys это сообщение участнику просто так не шлёт, зато начинает долбить жюри в консоль).
Что-нибудь вынести в отдельный файл и прекомпилировать его?
Вроде как существует, и это проделки ИТМО
(только вот зачем? polygon и testlib.h очень удобны, по-моему)
Либо могу выложить, если надо.
2) Выполняем команды:
javac -classpath testlib4j.jar Check.java -encoding utf8
jar cf Check.jar *.class
Спасибо, собрал jar-файл по Вашему рецепту, но и как же его теперь запускать?
Пытаюсь так:
java -jar Check.jar <in> <out> <ans>
Однако, ругается. Тогда гуглю ещё немного и пытаюсь так:java -classpath Check.jar Check <in> <out> <ans>
Результат выглядит примерно так:
<там много букв, посему он в правке>
И теперь уже выгуглить ничего не удаётся. Так что же я делаю неправильно?
Можно попробовать так - вроде следует из рецепта:
Будет неплохо, если на этот архив посмотрит кто-нибудь из грамотных людей и вынесет вердикт.
Прошу прощения, ответил по памяти :( Рекомендованный производителем способ все же такой:
На этот раз я его проверил, причем на предложенном архиве, честно-честно :) Похоже, за несколько лет борьбы с Java некоторые неочевидные вещи стали слишком привычными: подразумевал, что tetslib4j и check должны находиться в текущем каталоге, иначе надо прописывать точный путь к ним. Не могу придумать другой причины, по которой оно не работало бы.
Да ладно, я уже забил. Видимо, Java просто меня не любит.
Я очень высоко оцениваю и codeforces, и Polygon, и модификацию testlib, и благодарен за всю эту работу. И заранее прошу прощения, если мой вопрос окажется неприятным.
Тем не менее: почему на Полигоне нельзя прикручивать чекеры, написанные под checker.h, рекомендованный самим Черновым? Если у меня есть готовый и отлаженный чекер под checker.h, разве кому-то станет легче от того, что я буду вынужден переделывать, тратить время, и, возможно, допускать глупые технические ошибки?
Уважаемый Илья Николаевич!
Я был не прав, считая, что и сам Polygon, и informatics.mccme.ru всё равно каким-то боком завязаны на еджадж?
И ещё насчёт питона. АБСОЛЮТНО согласен, что НЕ СТ_О_ИТ связываться. На НетОИшном сервере от этого отсутствия обратной совместимости обычно прячутся, не обновляя старую версию. А где так сделать нельзя -- там начинается полный мрак...
exe -- это в смысле "исполняемый файл" без привязки именно к винде?
Да верю я, что еджадж работает с тестлибом! Вопрос в том, что для заливания на informatics.mccme.ru задач, к которым есть готовые отлаженные чекеры под checker.h , приходится эти самые чекеры переписывать под testlib.h .
Вышеприведённые аргументы более-менее убедили меня писать чекеры к новым задачам под testlib.h а не checker.h . Но не убедили, что переписывание готовых (но написанных под checker.h) чекеров будет не просто неудобством, а полезным занятием.
Хотя если на Полигоне действительно трудно разрешить checker.h -- что ж, будем подстраиваться под то, что есть...
void quitf(TResult result, const char * format, ...)
#ifdef __GNUC__
__attribute__ ((format (printf, 2, 3)))
#endif
Хотелось бы, чтобы в свежую версию testlib включили эту фичу.
In file included from check.cpp:1:0:
testlib.h: In member function 'int random_t::next(long long unsigned int)':
testlib.h:338:18: warning: comparison between signed and unsigned integer expressions
(Upd: в testlib 0.7.2 строка 339, а не 338.)
У меня проблема следующего рода: разрабатывая задачи, я прекрасно понимаю, что их будут использовать школьные учителя в 90% знающие Pascal и не знающие C. Да и в рассылке от ЦПМК по информатике на региональный этап тоже приходят Pascal-чекеры. С другой стороны, я "пытаюсь" использовать ejudge, который живет под Linux и я хотел бы иметь возможность с легкостью перекомпилировать чекер под Linux, используя fpc. В тех версиях testlib.pas, которые я видел всегда есть "uses windows". Не подскажите где взять Linux-версию testlib.pas?
Версия testlib-а на Паскале авторов из СПбГУ есть здесь: http://acm.math.spbu.ru/soft/testlib/ . Под linux чекеры должны компилироваться и работать. Есть пара несовместимостей с версией ИТМО (отсутствие NextLine?).