Update: Выпущен новый релиз 0.8.5. Скачать новую версию вы можете с сайта проекта. Новая версия внедрена во все сервисы Codeforces и Polygon. Изначально этот пост содержал информацию о девелопмент-версии, но сейчас он используется для описания версии 0.8.5.
Библиотека testlib — самая популярная и развитая библиотека для написания чекеров, генераторов, валидаторов и теперь интеракторов для задач. Первая версия testlib.h была написана мной в 2005-ом, и представляла по большей степени порт testlib.pas на С++. С тех пор многое было улучшено и расширено. В настоящий момент она активно используется в задачах Codeforces, разных этапах Всероссийской олимпиады школьников, многих петрозаводских контестах, всех саратовских соревнованиях и т.д.
Вот основные изменения/улучшения.
Комментарий программы: stdout -> stderr
Теперь системные сообщения testlib-программ (чекеров, валидаторов, интеракторов) направляются в стандартный поток ошибок (stderr) вместо стандартного потока вывода (stdout). Это сделано для идентичности поведения при написании интеракторов, стандартный вывод которых уходит в программу участника и не может использован для вывода системных сообщений. Это может повлиять на некоторые тестирующие системы, будьте внимательны!
Интеракторы
Появилась поддержка интеракторов. Напомню, что интерактор это программа взаимодействующая с решением участника при тестировании интерактивных задач. Интерактор читает одновременно содержимое теста (первый параметр командной строки, в интеракторе — это inf
), вывод программы участника (передается на стандартный ввод интерактора, в интеракторе — это ouf
), выводит данные в решение участника (в интеракторе выводите в cout
или используйте другой вывод, не забывайте flushes), выводит спец. файл для последующей обработки его чекером (вывод в поток tout
, который похож на cout
).
Пример интерактора:
#include "testlib.h"
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
setName("Interactor A+B");
registerInteraction(argc, argv);
// reads number of queries from test file
int n = inf.readInt();
for (int i = 0; i < n; i++)
{
// reads query from test file
int a = inf.readInt();
int b = inf.readInt();
// writes query to the solution, endl makes flush
cout << a << " " << b << endl;
// writes output file to be verified by checker later
tout << ouf.readInt() << endl;
}
// exit with message
quitf(_ok, "%d queries processed", n);
}
readStrictReal/readStrictDouble
У потоков inf/ouf/ans
появились функции readStrictReal/readStrictDouble (это одно и то же). Они очень строго читают вещественные числа, применимы в большей степени в валидаторах. Принимают аргументы:
- минимальное значение числа,
- максимальное значение числа,
- минимальное число цифр после десятичной точки (не обязательный),
- максимальное число цифр после десятичной точки (не обязательный),
- имя читаемой переменной (для вывода при ошибке).
Еще раз повторюсь — это нововведение нужно для валидаторов.
Защита от потерянных quit/quitf и readEof
Если вы пишите чекер, то легко забыть в какой-нибудь ветке выполнения сделать правильный quit
или quitf
. Раньше в этом случае чекер скорее всего завершался с exitcode 0 и для участника это означало вердикт "правильный ответ на тест". Теперь testlib автоматически при выходе проверит что был в самом деле вызван quit/quitf (это работает для чекеров) и аварийно завершит работу, если вызова не было. Аналогично, если валидатор не сделал чтение конца файла (readEof
), то произойдет аварийное завершение.
quitif
В чекерах (и не только) очень популярна конструкция вида:
if (answer != output)
quitf(_wa, "Expected %d, found %d", answer, output);
Теперь ее можно записать чуть короче, используя функцию "выйти если" (quitif): quitif(answer != output, _wa, "Expected %d, found %d", answer, output);
Улучшение производительности
Вместо слов короткий пример. Запускаем простой чекер сравнения миллиона целых чисел (вывод и ответ ~ 7.5 мегабайт, для C++ использовался g++ 4.6.1, а для pascal — Delphi 7):
- предыдущий релиз testlib.h — 2.8 секунды,
- паскалевский testlib от ИТМО — 3 секунды,
- паскалевский testlib от СПбГУ — 4 секунды,
- новая версия testlib.h — 0.35 секунды
Таким образом, можно утверждать, что мнение о медленной работе предыдущих версий — просто миф, а новый testlib быстрее предыдущего примерно в 8 раз!
Аналогичные измерения, если использовать текстовые чекеры (сравнения строк или токенов):
- предыдущий релиз testlib.h — 1.2 секунды,
- паскалевский testlib от ИТМО — 3.5 секунды,
- паскалевский testlib от СПбГУ — 2 секунды,
- новая версия testlib.h — 0.25 секунды.
Миф подтверждается, а время работы сократилось вдвое. Видимо, на больших файлах скорость работы возросла примерно в 3-6 раз.
Функции-помощники
Добавлены несколько простых полезных функций (некоторые уже были):
lowerCase(std::string)
,upperCase(std::string)
— перевести строку в нижний или верхний регистр,trim(std::string)
— удаляет слева и справа строки пробелы, табы и символы перевода строк,vtos(T)
— шаблонная функция, превращает экземпляр T в строку (медленная, использует stringstream),format(шаблон, параметры)
, напримерformat("n=%d", 5)
— возвращает строку по шаблону и параметрам,englishEnding(int)
— правильное окончание для числительного, например englishEnding(2)="nd",compress(std::string)
— заменяет длинную строку более короткой, оставляя ее префикс и суффикс, а между ними многоточие, очень полезна при выводе в комментарий какого-либо вывода участника,startTest(int)
— используется в мультигенераторах, фактически делает freopen(имя-тест, "r", "stdin"), то последующий вывод перенаправляется в файл с именем равным номеру теста.
Мелочи
Макросы I64, U64
Теперь определены макросы:
#ifdef ON_WINDOWS
#define I64 "%I64d"
#define U64 "%I64u"
#else
#define I64 "%lld"
#define U64 "%llu"
#endif
Пример использования: printf("n=" I64 ", m=%d", n, m)
. Используйте их для ввода-вывода 64-битных знаковых/беззнаковых чисел через scanf/printf
. Хотя такое делать почти не надо, так как чтение происходит через inf/ouf/ans
, а при выводе лучше сделать vtos(n)
.
Случайный элемент коллекции (отрезка итераторов).
Теперь можно в генераторе написать так: cout << rnd.any(v) << endl
, где v
— это вектор. Так же поддерживается вариант с парой итераторов random_t::any(begin, end)
, т.е. произвольный элемент массива можно получить так rnd.any(a, a + n)
.
Issue
Исправлен issue с пространством имен около iter_swap
, из за чего были проблемы с компиляцией на некоторых GCC g++.
Новый члены-функции у InStream: readWordTo/readTokenTo/readStringTo/readLineTo
Такие функции были добавлены для ускорения чтения в чекерах (валидаторах и т.д.), где приходится очень много читать. Например, вот так может выглядеть основной код чекера, который сравнивает ответы и вывод по словам (токенам):
string j, p;
while (!ans.seekEof() && !ouf.seekEof())
{
n++;
ans.readWordTo(j);
ouf.readWordTo(p);
if (j != p)
quitf(_wa, "%d%s words differ - expected: '%s', found: '%s'", n, englishEnding(n).c_str(), compress(j).c_str(), compress(p).c_str());
}
InStream::quitf
Добавлен InStream::quitf
, теперь. Помните, что для потоков ans/inf
любая причина выхода, отличная от _fail
приводит к _fail
. Например, следующий участок кода корректен
int readAnswer(InStream& in)
{
int n = in.readInt();
if (n % 2 != 0)
in.quitf(_wa, "n should be even, but %d found", n);
return n;
}
int main(int argc, char * argv[])
{
registerTestlibCmd(argc, argv);
int ja = readAnswer(ans);
int pa = readAnswer(ouf);
quitif(ja > pa,
_fail, "expected %d, found %d", ja, pa);
quitif(ja < pa,
_wa, "expected %d, found %d", ja, pa);
quitf(_ok, "answer is %d", ja);
}
Если выведено нечетное число в файл с ответом, то будет _fail
, а если в вывод участника — то _wa
.
Стандартные чекеры/валидаторы
В стандартные поставляемые чекеры были добавлены два чекера: caseicmp.cpp
и casencmp.cpp
, которые работают в случае стандартной для ACM-ICPC Finals разметки multitestcase. Первый из них поможет, если надо проверять одно целое число на testcase, а второй — если последовательность чисел.
Тестирование
Новый код усиленно тестировался, в частности в полуавтоматическом режиме на большом количестве тестов. Сравнивалось, что stdout/stderr и exitcode чекера не изменился после его компиляции с новым testlib.h.
Раз уж появился
I64
, то можно за компанию добавить иU64
("%I64u"
/"%llu"
) дляunsigned long long
.Хорошая идея, добавил.
У меня не компилится. (Видимо из-за 64битности системы size_t = long int)
ncmp.cpp и testlib.h из транка
Ubuntu 12.10 x64
Раньше компилировалось? В любом случае, исправлю.
Раньше у меня была 32 битная система
У меня ещё выводит такие предупреждения:
Видимо, в класс
InputStreamReader
надо добавитьДа, конечно.
Вспомнилась еще одна старая проблема тестлиба, которую можно бы попробовать решить.
Он слишком долго компилируется. Видимо решается это так. Из него выделяется собственно .h, в котором все объявления. А все остальное выделяется в .cpp, который можно один раз скомпилировать в библиотеку.
Спасибо за новую версию. Особенно за "в 5 раз быстрее " :-)
На всякий случай дам альтернативную статистику на тему "мнение о медленной работе предыдущих версий — просто миф". У меня под рукой был kitten-testlib (СПБГУ, pas). Вспоминаем, что под fpc скорость работы testlib выше, чем под dcc. Далее статистика:
Видно, что старый testlib.h медленней в 3 раза и в infinity раз дольше компилится. Новый testlib.h быстрее в 1.3 раз. И это круто :-)
Тем не менее, это не предел скорости. В качестве эксперимента, возвращаясь со школьных сборов, где почти каждый день новые задачи на 50-100 больших тестов и часто хочется б**о**льшей скорости от валидатора и чекера... Так вот, в качестве эксперимента я написал быстрый аналог testlib.h, совместимый со старым. Далее статистика:
Принципиальное отличие "быстрого my_testlib.h" в том, что он не использует нигде STL (включая string). Посмотреть код можно здесь my_testlib (readme)
Вся статистика приведена для теста "106 целых чисел по модулю не более 109". Способ запуска: validate < a.in и check emptyinput a.in a.in
Кстати, раньше на сборах для повышение скорости я иногда пользовался вот такой заплаткой: patch
P.S. Для "testlib.h" есть более жесткий тест — это валидация 107 слов, каждое из одной буквы. Версия 0.8.3 работает 5.22 секунды, my_testlib работает 0.87 секунд. Файл весит 20M.
Про FPC не знал, но первый попавшийся чекер после компиляции на FPC стал работать неправильно :-( У FPC есть ряд несовместимостей с Delphi вокруг ввода-вывода и в чекерах это бывает заметно. Мы неоднократно на это напарывались в чекерах для тренировок.
Добавил в testlib.h функции вида readWordTo/readTokenTo/readStringTo/readLineTo, ncmp стал работать 0.35 сек (предыдущий релиз 2.8 секунды, версия на момент написания поста 0.55 сек), а wcmp стал работать 0.23 сек (предыдущий релиз 1.2 секунды, версия на момент написания поста 0.65 сек).
Кстати, my_testlib не могу проверить на больших файлах (как в комменте, 20М), так как его буфер переполняется. Попробовал адаптировать ncmp к использованию my_testlib, получилось 0.6 сек.
Попробовал wcmp на 107 строках с буквой
a
, он сейчас работает 1.4 сек против 10 секунд для версии 0.7.4.Сергей, можно подробнее что такое валидация такого файла и что именно написано в твоем валидаторе?
Я правильно понял, что последнее ускорение получено благодаря ним? Если так, это круто, спасибо =)
Только почему они все вида
void readWordTo(std::string& result);
?Мне казалось, кучу тормозов создают как раз string-и...
Да... там буффер всего 107. Можешь его руками увеличить :-) Я так некрасиво сделал потому что это таки самый быстрый способ "выделить" себе много памяти. Завести глобальный массив. Именно глобальный не static массив.
Видимо, правильней делать
readWordTo(char *s, size_t maxLen)
, тогда сколько нужно памяти и как ее выделять решит юзер.Да, конечно. Вот код валидатора.
Статистика на моем компе такая: N = 107, testlib.0.8.3 = 5.22 sec, my_teslib = 0.87 sec
P.S. Меня еще всегда удивляло, что есть
quit
,quitf
,InStream::quit
, но нетInStream::quitf
. Было неудобно. Отдельное спасибо, что в 0.8.* оно появилось :)Да, многое ускорилось, когда строки стали поменьше создаваться/копироваться.
Можно, конечно, еще char*-варианты сделать, но ускорят они уже не сильно.
Статистика на твоем компе непонятна. Я воспроизвел у себя твой кейс с валидацией. Результат такой (win7, g++ 4.6.1, ssd):
Компилировал так: g++ -Wall -O2 -Wl,--stack=256000000 -static -o v v.cpp
Конечно, практически plain C вариант чуток шустрей, но разница не принципиальна.
Разница порядка 10% таки может значить разницу между "judge всё успевает" и "накапливается получасовая очередь". Особенно если учесть психологический эффект: "что-то у меня долго не проверяют — перепошлю, мало ли что".
А за счет чего увеличилась скорость работы в новой версии?
Думаю за счет буферизации ввода.
Ручной буфер чтения, уменьшил кол-во мест где данные копируются.
Итмо так-то на java чекеры/валидаторы давно переходит
Can you try to translate it to English? I can only read the sample code...
Встроена ли поддержка нового testlib'а в Contest Wizard? Можно ли уже включать интерактивные задачи в контест из Визарда? Последний раз 2 недели назад, когда я это пробовал, у меня не получилось. Если нельзя, то как все-таки добавлять интерактивные задачи?
Да, сейчас все встроено. Обратите внимание, что интерактор читает в качестве теста argv[1], а пишет в argv[2]. Например, в задачах ITMO это не всегда так. Кроме того, если он выводит не exitcode=0, то он трактуется как и exitcode чекера (например, exitcode=1 это wrong answer).
А можно где-нибудь почитать, как устроены интерактивные задачи в полигоне? А то я всё никак не пойму, почему решение http://pastebin.com/YkUtyLZt получает WA, а http://pastebin.com/rjHebe82 — TL.
В новой версии падают чекеры, которые передают
InStream
в функцию.Лечится передаванием
InStream&
вместо него.В общем-то это логично, я и раньше не понимал, как оно работало.
Видимо, лучше вообще запретить у него конструктор копирования и оператор присваивания. Чтобы такие чекеры даже не компилировались.
А разъясните, пожалуйста: то, что под Visual C++ 2008 Express Edition не компилируются проверялки/генераторы/... с использованием testlib.h — это проблема настроек студии, проблема конкретной версии студии, или добиться работы таковых генераторов из-под студии вообще сложно и так и задумано?
Конкретные ошибки:
Quick fix: вставить throw в конец этой функции rand().
извините, не понял. можно чуть подробнее?
Дописать строку "throw" в функцию rand() в testlib.h. Студия ругается на то, что функция не содержит return, хотя должна возвращать int.
Ругается она на несоответствие заявленных exception с предыдущим определением функции. Что вроде пофиксили в послдней версии тестлиба. Попробуйте поставить его первым include.
Нет, то был warning, он всегда был и остался. Сейчас же к нему добавилась фатальная ошибка компиляции.
Упс. Незаметил последнюю строку.. Ну под g++ эта проблема полностью полечена сейчас. С MSVS хуже. А о чем первые две ошибки кстати?
Вы уверены, что речь о последней версии testlib? Специально установил все версии Visual Studio от 2005 до 2012. Примеры из архива компилируются на всех.
У меня не компилируется даже просто "#include "testlib.h"", testlib взял 0.8.5 из свежесозданной в полигоне задачи, компилятор Visual Studio 2012 + November CTP. Может, в полигоне не самая последняя версия?
А как из Полигона взял? Лучше прям из пакета взять, а не копи-пейстнуть из textarea (так могут быть проблемы с кавычками или что-то такое). Кинь мне лог компиляции.
Мне кажется, было бы удобно в полигоне видеть коммент интерактора, не только коммент чекера. (в Invocations, в т.ч при ОКе)
Баг-репорт.
Строчка 321:
return ((nextBits(31) << 32) ^ nextBits(31));
32-й бит всегда получается нулевой.
Должно быть хотя бы:
return ((nextBits(31) << 32) ^ nextBits(32));
Из-за этого при генерации
rnd.next(-(1LL<<31),(1LL<<31)-1);
всегда получаются отрицательные числа.
Может тогда правильно так:
return ((nextBits(32) << 32) ^ nextBits(32));
? Ну и проверку снизу ослабить до 64.nextBits(63)
используется вnext(long long)
.Ваш вариант может возвращать отрицательное число.
Но там стоит
do { bits = nextBits(63); } while (bits >= limit);
так что без разницы.
Вообще, по идее надо обрезать первые
bits
битов, чтобы оно соотвествовало названию, но так как онаprivate
и используется только cbits = 26, 31, 63
, то, опять же, это не так важно.Исправил на предложенный первоначальный вариант. В ближайшем релизе появится.
А это не плохо, что меняется поведение рандома?
В данном случае это конкретный баг. Видимо, лучше поменять. Понятно, что влияет это на поведение в довольно экзотических случаях вроде того, что указал Anton_Lunyov
Улучшенная защита от случайных выходов в чекере (действительно уменьшающая вероятность случайного, помимо воли автора чекера, вердикта OK) становится головной болью при попытке написать оценивающий чекер, то есть ставящий отдельному запуску решения на отдельном тесте целочисленную оценку от 0 до максЗаТест.
Используйте quitp, передавая в него целочисленный балл.
Он выходит не с тем кодом возврата. Для этого было бы удобно, если бы можно было переопределять коды. Я что-то тебе писал про это, но ответа не получил.
Я не очень понял, что ты имел ввиду. Давай попробуем еще раз :)
У тебя опредляются константы вроде OK_EXIT_CODE. Я предлагаю их не определять если они уже определены, например из строки компиляции, или как костыль из кода чекера. Это позволит решить большинство таких проблем.
Вроде того, что протаскивать значения из defines вида -DOK_CODE=0 -DWA_CODE=1 в константы, если такие defines определены?
Ну например так. Хотя я имел ввиду чтото вроде
Есть вопрос по интерактору. Взял из примера интерактор, написал такое авторское решение:
При его тестировании получаю FL:
Verdict FL (verdict FL violates solution's tag MAIN)
Comment Interactor 'i.cpp' returns exit code 3 [FAIL Answer file not found: "C:\Contest..."]
Что я делаю не так?
UPD. Checker: std::ncmp.cpp
Могу расшарить работающую интерактивку
Буду благодарен. Ник такой же.
registerInteraction ?
В примере он есть.
Если еще не разобрался, то пошарь на меня эту задачу, я разберусь.
А на какой ник?
Там с тестлибом что-то. На 0.9.5 работает, на последней не работает
Я расшарил dalex, он глянул и обновил testlib.h из своей задачки с интерактором и все заработало. С testlib.h все хорошо сейчас?
Я хочу разобраться с проблемной, пока не смог воспроизвести. Я пошарил на тебя задачу
interactive-a-plus-b
, которую только что с нуля создал. В ней всё работает. Как добиться, чтобы сломалось?Когда планируется переезд testlib на github?
Точно до дедлайна. А это на что-то функционально влияет?