Рабочий стол > DL Руководство пользователя > ... > Вспомогательные средства установки задач по программированию > Вспомогательные инструменты для установки задач по программированию в системе «Дистанционного обучения в Беларуси» > Просмотр
Вспомогательные инструменты для установки задач по программированию в системе «Дистанционного обучения в Беларуси» Войти | Зарегистрироваться   Просмотр версии для печати текущей страницы.

Добавлено Гуленко Алексей, последний раз изменено Гуленко Алексей Apr 07, 2013  (просмотр изменений)
Метки: 
(нет)

Вспомогательные инструменты для установки задач по программированию в системе «Дистанционного обучения в Беларуси»

Содержание

Введение
1 Распространённые проблемы, возникающие при установке задач
1.1 Проблемы установки тестов
1.2 Проблемы тестирования решений
2 Описание использованных средств
3 Использование разработанных инструментов
3.1 Инструменты для автоматизации установки задач
3.1.1 Приложение ZeroInc
3.1.2 Приложение TaskConv
3.2 Инструменты для тестирования интерактивных задач
3.2.1 Приложение StdSwp
3.2.2 Библиотека DL_I и стандартный чекер для интерактивных задач
Заключение
Список использованных источников
Приложение А. Исходный код приложения ZeroInc
Приложение Б. Исходный код функции проверки принадлежности имени файла шаблону в приложении TaskConv
Приложение В. Исходный код приложения StdSwp
Приложение Г. Исходный код библиотеки DL_I
Приложение Д. Исходный код стандартного чекера для интерактивных задач
Приложение Е. Пример исходного кода player'а с использованием библиотеки DL_I

Введение

В настоящее время в сети Гомельского Государственного Университета, а также в Internet функционирует система, которая была разработана с целью предоставить возможность пользователям не выходя из дома повышать свой уровень образования в различных дисциплинах, как в практике, так и в теории. Данная система появилась в 1999 году и известна под названием «Дистанционное обучение в Беларуси» (Distance Learning Belarus). Обучение в системе происходит путём организации обучающих курсов.

Система «Дистанционного обучения в Беларуси» изначально разрабатывалась для тренировки участников в олимпиадах по информатике (программированию), и данная цель до сих пор остаётся одной из основных. В процессе тренировки участники решают т.н. задачи по программированию; для большинства таких задач участнику предлагается отправить на тестирующий сервер системы код программы, выполняющий вычисления, цель которых описана в условии задачи. Программа компилируется и прогоняется через набор тестов; число баллов, получаемых за задачу, соответствует количеству и (иногда) сложности тестов, на которых тестирование программы дало положительный результат; программа, прошедшая все тесты с положительным результатом, получает максимальный балл, предлагаемый для данной задачи.

В системе «Дистанционного обучения» большая часть задач по программированию устанавливается из архивов реальных олимпиад, проводимых в Internet либо локально. Это, с одной стороны, практически убирает необходимость в разработке новых задач данного типа (олимпиады по программированию проводятся весьма часто), с другой же - даёт участникам олимпиад по информатике возможность тренировки на задачах требуемого уровня (в частности, они могут решать задачи, предлагавшиеся в прошлые годы на олимпиаде, к которой они готовятся).

Тем не менее, данный подход может вызывать некоторые трудности в его реализации. Суть проблемы в том, что большинство олимпиад проводится на собственных тестирующихся системах, разрабатывавшихся независимо друг от друга, и имеют различные структуры представления тестов; и, хотя для переноса задач в системе «Дистанционного обучения» давно используются инструменты для автоматизации процесса, иногда они могут давать ошибочные результаты, либо не иметь нужных возможностей.

Также, существует класс задач, на которые система «Дистанционного обучения» исходно не рассчитана: т.н. интерактивные задачи. Их установка требует индивидуального подхода; большинство из них требовалось устанавливать вручную, и до недавнего времени их установка требовала большого объёма работы.

В данной курсовой работе была поставлена задача разработки дополнительных инструментов, уменьшающих работу по установке задач, которые не удалось установить корректно стандартными средствами. Это позволит снизить количество труда по установке этих задач вручную, потенциально - автоматизировать процесс их установки.

1 Распространённые проблемы, возникающие при установке задач

Проблемы, возникающие при установке задач по программированию в системе «Дистанционного обучения в Беларуси» из архивов реально проходивших олимпиад, как правило, относятся к одной из двух разновидностей: проблемы установки тестов и проблемы тестирования решений.

1.1 Проблемы установки тестов

При организации новой олимпиады система тестирования задач, как правило, пишется с нуля. Это правило не выполняется только если один несколько олимпиад организуются одним и тем же организатором - в этом случае для всех олимпиад, кроме первой, может быть использована одна и та же система тестирования.

Причиной тому является ограниченный доступ к исходным кодам олимпиадных систем тестирования и отсутствие свободно распространяемых систем; в каждом случае система тестирования разрабатывается по индивидуальному заказу. В сочетании с отсутствием каких-либо общих стандартов на формат задач, это приводит к тому, что наборы используемых файлов, их названия, размещение и содержание разнятся от олимпиады к олимпиаде.

При установке задачи определяется файл с условием и файлы с тестами, после чего они переименовываются для определения системой «Дистанционного обучения». Далее, конфигурация тестирования заносится в файл task.cfg; сюда входят имена входных файлов, указанных в условиях, ограничения по памяти и по времени и ограничения максимальных баллов по тестам; также здесь может быть указан, в частности, тип задачи (если она должна тестироваться каким-то особым образом) или пометка об использовании нестандартного чекера (который в таком случае должен быть помещён в папку с установленной задачей).

Большая часть описанных действий производится автоматически при импортировании задачи в систему. Однако, распознание и переименование тестов не всегда выполняется корректно.

Тесты в системе «Дистанционного обучения» представлены следующим образом: для каждого теста даются входной и выходной файл (файл со входными и с проверочными данными соответственно) и строчка в файле task.cfg, содержащая количество баллов, начисляемое тестируемой программе за полное прохождение теста; входной файл называется N.in (где N - номер теста), выходной - N.out; если на вход требуется предоставить более одного файла, эти файлы помещаются в подпапку с названием N.in. Также, тесты могут быть разбиты на группы (т.н. групповые тесты) - в этом случае тест может считаться пройденным только если пройдены все тесты в группе; в системе «Дистанционного обучения» эти группы отмечаются в файле task.cfg: каждый тест, входящий в одну группу со следующим, отмечен как имеющий отрицательное число баллов (т.о., тест с положительным числом баллов отмечает конец группы).

Одной из ошибок при установке задач является ошибка в нумерации тестов - они должны нумеруоваться с единицы, но при такой ошибке нумерация может начаться с нуля. В такой «нулевой» тест в дальнейшем попросту игнорируется.

Другой проблемой является распознание групповых тестов - как правило, система распознаёт лишь часть из них, и распознанные тесты определяются как обычные (без объединения в группы).

1.2 Проблемы тестирования решений

Большинство устанавливаемых задач тестируются следующим образом: код решения компилируется и многократно запускается. При каждом запуске ему на вход (в стандартный поток ввода либо во входной файл, в зависимости от задачи) выдаются исходные данные, затем тестирующая система выжидает отмеренное время и, если программа к этому времени завершила работу, выданный ею результат сравнивается с содержимым эталонного входного файла. Если требуется более сложная проверка (скажем, количество правильных решений превышает единицу), при установке задачи в папку с тестами помещается программа-чекер, которая и выполняет проверку (имя файла - checker.exe), а в файле task.cfg делается соответствующая пометка.

Однако, существуют нестандартные типы задач. В частности, это задачи, в которых тестирование происходит путём обмена данными с проверяющей программой (для удобства назовём её player'ом) в реальном времени (т.н. интерактивные задачи).

При проектировании системы «Дистанционного обучения в Беларуси» возможность тестирования таких задач не учитывалась. Поэтому их установка до недавнего времени не представлялась возможным.

В 2006 году была написана программа stdswp для соединения потоков ввода-вывода тестируемой программы и player'а и описана ручная установка интерактивной задачи в системе «Дистанционного обучения». Позднее был введён новый тип задачи (type=interactive) для системы тестирования, процедура тестирования которого предполагала использование программы stdswp; она поддерживала обмен данными между запущенными ею решением и player'ом, и продолжала работу пока работает хотя бы одна из этих программ.

Однако, условия многих интерактивных задач предполагают иное поведение тестирующей системы. Поэтому такие задачи нельзя было тестировать с помощью этой программы.

Кроме того, поставляемые с задачами player'ы (которые не всегда имеются в наличии) как правило, не совместимы с системой «Дистанционного обучения». Поэтому обычно приходится писать их с нуля; также для каждого из них обычно нужно добавлять индивидуальный чекер.

2 Описание использованных средств

Для реализации проектируемых инструментов был выбран Free Pascal, т.к. программы на языке Pascal удобно отлаживать, а реализованный на различных платформах компилятор (со средой и библиотеками) упрощает перенос разрабатываемых программ.

Free Pascal включает следующие наборы библиотек: RTL (Run-Time Library) для реализации доступа к базовым функциям ОС и файловой системы (приблизительно совпадает по области применения с STL языка C++), FCL (Free Class Library) для реализации вспомогательных классов (работа с pipe'ами, реализация AVL-деревьев, работа с БД и т.п.), и LCL (Lazarus Component Library) для реализации визуальных компонентов GUI (LCL поставляется с Lazarus IDE для компилятора FreePascal).

Для упрощения реализации были использованы следующие библиотеки RTL:

  • SysUtils (функции GetDir для определения рабочей папки; ChDir для смены папки; FileExists для проверки существования файла/папки, DirectoryExists для проверки существования папки; FindFirst, FindNext и FindClose для перебора файлов в папке; RenameFile для переименования/перемещения файлов/папок; AnsiLowerCase для понижения регистра в путях к файлам);
  • GetOpts (тип TOption, функция GetLongOpts и константа EndOfOptions для работы с опциями командной строки).

Также были использованы следующие библиотеки FCL:

  • Classes (класс TFileStream для безопасного копирования файлов);
  • Process (класс TProcess для запуска и контроля процессов).

3 Использование разработанных инструментов

3.1 Инструменты для автоматизации установки задач

Для устранения проблем установки задач были разработаны два приложения: ZeroInc для устранения «нулевых» тестов и TaskConv для конвертирования задач с групповыми тестами в формат системы «Дистанционного обучения».

3.1.1 Приложение ZeroInc

Применение. Программа ZeroInc (Zero Increment) запускается из командной строки, в качестве параметров она получает список обрабатываемых папок (при запуске без параметров она обрабатывает рабочую директорию - ту, откуда была запущена):

zeroinc.exe [Путь1 [Путь2 [...]]]

Поведение. При обработке папки она проверяет наличие «нулевого» теста и, в случае обнаружения, переименовывает все найденные в папке входные файлы (или подпапки) тестов и соответствующие им выходные (если таковые есть), увеличивая номер теста на 1.

Исходный код программы содержится в приложении А.

3.1.2 Приложение TaskConv

Применение. Программа TaskConv (Task Convertor) запускается из командной строки и принимает следующие параметры:

  • -h (--help) - программа выводит краткую справку по параметрам ввода и завершает работу (также выполняется при неправильно заданных параметрах);
  • -v (--verbose) - программа выводит в консоль информацию о выполняемых действиях (используется для контроля работы программы в случае ошибки);
  • -q (--quiet) - по эффекту противоположно -v (если используются обе опции, эффективна лишь последняя из введённых), отменяет вывод в консоль (по умолчанию выводятся название обрабатываемой папки и сообщение в случае ошибки);
  • -m (--move) - файлы перемещаются без сохранения оригиналов (по умолчанию они копируются);
  • -t (--type) TaskType - уточняет имя типа формата задачи (при определении типа программа игнорирует те, которые не начинаются с TaskType; подробнее о типах см. Конфигурация);
  • -o (--output) OutFile - изменяет имя выходного файла программы (по умолчанию marks.tmp);
  • -n (--name) TaskName - фиксирует значение TaskName (см. Конфигурация);
  • -d (--directory) TaskDir - задаёт путь к папку с конвертируемой задачей (обязательный параметр).

Параметры могут быть использованы как в короткой, так и в длинной форме, причём их названия можно сокращать вплоть до одной буквы.

Поведение. Приложение TaskConv работает следующим образом:

  • обрабатывается список параметров (если один параметр с аргументом задан дважды, происходит аварийное завершение программы);
  • считывается конфигурация из файла .cfg и директории DB, которые должны находиться в рабочей папке;
  • программа пытается определить тип задачи по количеству подходящих к шаблону входных и выходных файлов (чем больше входных файлов, тем лучше; их число должно совпадать с числом выходных);
  • в случае успешного определения типа формата задачи входные и выходные файлы переименуются, попутно составляется список тестов по группам;
  • список тестов выводится в выходной файл программы (см. -o) в формате, подходящем для занесения в task.cfg (стоимость каждого теста считается за 1 балл).

Конфигурация. Типы формата задачи описаны в файлах, помещённых в папку DB. В первой строке описан шаблон пути входного файла, во второй - соответствующего выходного. Шаблон представляет собой путь к файлу из папки задачи, в котором части пути, различающиеся для разных тестов, обозначены метками-«переменными».

Каждая переменная заключается в фигурные скобки, предваряемые знаком «$» (${переменная}). Исключением могут быть переменные, обозначающие номер теста в группе (если в группе из одного теста этот номер не указывается, переменная заключается в квадратные скобки - $[переменная]). Распознаются следующие переменные:

  • TaskName - имя задачи;
  • S - номер группы тестов;
  • SS - номер теста в группе;
  • SL - буква теста в группе (нумерация латиницей).

TaskName определять сложнее всего и, в случае неправильно составленного шаблона, оно может быть определено неправильно. Во избежание подобных ситуаций добавлена возможность жёсткого фиксирования его значения (см. -n); также, программа считает, что в TaskName могут входить только символы, указанные в первой строке файла .cfg (именно для этого он и предназначен) - по умолчанию это все буквы латиницы.

Примеры. Пример файла шаблона для задачи с олимпиады IOI:

DB/IOI
${TaskName}-test/subtask${S}/grader.in.${SS}
${TaskName}-test/subtask${S}/grader.expect.${SS}

То есть, если задача называется race, то её тесты размещены в подпапке race-test; если у задачи 4 группы тестов, то внутри race-test лежат папки subtask1, subtask2, subtask3 и subtask4; если в первой группе 3 теста, то в папке race-test/subtask1 лежат файлы grader.in.1, grader.in.2, grader.in.3, grader.expect.1, grader.expect.2 и grader.expect.3; эти файлы будут скопированы (перемещены) в корень папки задачи с именами 1.in, 2.in, 3.in, 1.out, 2.out и 3.out соответственно, а в строки с 1 по 3 выходного файла программа выведет числа -1, -1 и 1 соответственно.

Пример файла шаблона для задачи с олимпиады CEOI:

DB/CEOI
${TaskName}${S}$[SL].in
${TaskName}${S}$[SL].out

То есть, если задача называется bal, имеет 5 групп тестов с номерами 0-4, в группах с номерами 0-2 один тест, а в остальных по два, то файлы тестов будут находиться в корне папки задачи и иметь названия bal0.in, bal0.out, bal1.in, bal1.out, bal2.in, bal2.out, bal3a.in, bal3a.out, bal3b.in, bal3b.out, bal4a.in, bal4a.out, bal4b.in и bal4b.out; эти файлы будут переименованы в 1.in, 1.out, 2.in, 2.out, 3.in, 3.out, 4.in, 4.out, 5.in, 5.out, 6.in, 6.out, 7.in и 7.out соответственно, а в выходной файл программа выведет числа 1, 1, 1, -1, 1, -1 и 1 соответственно.

Исходный код функции проверки принадлежности имени файла шаблону содержится в приложении Б.

3.2 Инструменты для тестирования интерактивных задач

Для тестирования интерактивных задач была разработана программа StdSwp, имеющая продвинутые по сравнению со своей предшественницей возможности, позволяющие варьировать поведение тестирующей системы от задачи к задаче. Также была разработана библиотека DL_I для упрощения написания player'ов к интерактивным задачам и чекер, годный на проверку задач, в player'ах которых была использована данная библиотека.

3.2.1 Приложение StdSwp

Применение. Приложение StdSwp (Standard Swap) запускается тестирующей системой для задач, которые в task.cfg отмечены как имеющие интерактивный тип (type=interactive). В качестве параметров ей передаются имена файлов player'а и тестируемой программы. Файл player'а должен находиться в папке задачи (файл player.exe); также для корректной обработки вывода player'а желательно наличие чекера (см. подр. 1.2).

Поведение. Приложение StdSwp работает следующим образом:

  • считывается и обрабатывается файл конфигурации (см. Конфигурация);
  • запускаются указанные в параметрах файлы (player и тестируемая программа), стандартные потоки ввода-вывода (Input/Output в Pascal, stdin/stdout в C/C++) этих программ перенаправляются в буфер StdSwp;
  • пока работают обе программы, StdSwp продолжает работу, выполняя обмен данными между программами;
  • когда одна из программ завершает работу, StdSwp перенаправляет её оставшийся вывод на вход другой программе и (по умолчанию) ожидает её завершения.

Конфигурация. Детали поведения программы StdSwp можно задавать, поместив в рабочую папку файл stdswp.cfg; для этого следует поместить его в папку задачи и сделать в task.cfg пометку о необходимости его копирования в директорию тестирования:

task.cfg
checksubject=file
checkfiles={stdswp.cfg}

Файл stdswp.cfg обрабатывается по следующим правилам:

  • каждая строка обрабатывается отдельно;
  • если в строке есть символ «#», все символы в строке начиная с него игнорируются (комментарий);
  • если в строке (вне комментария) отсутствует символ «=», она игнорируется (не соответствует формату);
  • оставшиеся строки считаются представленными в следующем формате:
    ИмяОпции=Значение

Программа распознаёт следующие опции:

  • KillSolution (возможные значения Yes и No) - при значении Yes работа тестируемой программы завершается принудительно после завершения работы player'а, значение по умолчанию - No;
  • KillPlayer (возможные значения Yes и No) - при значении Yes работа player'а завершается принудительно после завершения работы тестируемой программы, значение по умолчанию - No;
  • ListenError (возможные значения Yes и No) - при значении Yes тестирование прерывается принудительно, если player делает вывод в stderr (предназначена для прерывания тестирования при некорректном выводе тестируемой программы, также ловит ошибки player'а), значение по умолчанию - Yes.

Примеры. Пример файла stdswp.cfg:

stdswp.cfg
# завершить программу после остановки player'а
KillSolution=yes
# раскомментировать для завершения player'а
# после остановки программы
#KillPlayer=YES

Важные моменты. Существуют некоторые особенности, которые следует учитывать при тестировании интерактивных задач с помощью программы StdSwp:

  • поскольку стандартные потоки ввода-вывода перенаправляются системными средствами (pipe'ами) в буфер другой программы, для работы с файлами их использовать нельзя - вместо них нужно использовать файловые переменные (в Pascal) или файловые потоки (в C/C++);
  • размер буфера ограничен, поэтому, если одна из программ делает многократный вывод, а другая этот вывод не читает, рано или поздно буфер будет переполнен, и обмен данными прекратится (обе программы, скорее всего, «зависнут» в ожидании ввода);
  • похожая ситуация может произойти при неправильно написанном player'е или решении - каждая из двух программ будет ожидать вывода от другой (зачастую причиной ошибки может быть отсутствие Flush(Output)/flush(stdout) после операций вывода: если не принуждать программу к завершению вывода перед чтением, другая программа будет ожидать завершения вывода);
  • операции ввода-вывода сами по себе занимают сравнительно длительное время, поэтому оценка длительности работы программы при множественном обмене данными вырастает на пару порядков;
  • если тестируемая программа делает некорректный вывод, player'у следует отразить это в файле вывода, и вывести что-нибудь в stderr (см. ListenError); если этого не сделать, то программа, скорее всего, будет ожидать ввода, пока тестирующая система не завершит её работу (в этом случае система «Дистанционного обучения» игнорирует файл вывода и сообщает, что программа была снята с выполнения по превышении лимита времени);
  • если тестируемая программа завершила работу, а player продолжает ждать вывода, он будет снят с выполнения по превышении лимита времени с выдачей соответствующего сообщения;
  • при групповом тестировании интерактивных задач сообщения чекера игнорируются (при реализации интерактивного типа возможность группового тестирования не учитывалась).

Исходный код программы содержится в приложении В.

3.2.2 Библиотека DL_I и стандартный чекер для интерактивных задач

Применение. Библиотека DL_I реализует базовые функции player'а при тестировании интерактивных задач; другими словами, она подключается player'ом при компиляции и сокращает объём вводимого с нуля кода.

На данный момент библиотека реализована на Pascal, но реализовать её на других языках не составит труда.

Библиотека подключается обычным для языка методом. В Pascal это делается так:

Uses
  DL_I;

(команда Uses размещается в начале программы, после заголовка - если он есть, - в единственном экземпляре; после неё следует список идентификаторов библиотек).

После подключения библиотеку следует инициализировать (см. Интерфейс).

Библиотека берёт на себя взаимодействие с файлом вывода, позволяя программисту сосредоточиться на взаимодействии с тестируемой программой. Также для player'ов, использующих DL_I, был реализован чекер, умеющий обрабатывать её вывод (стандартный чекер для интерактивных задач).

Интерфейс. Библиотека DL_I предоставляет следующие константы:

  • Failed - пометка о непринятом тесте (см. RageExit);
  • Success - пометка о принятом тесте и выводимое чекером сообщение по умолчанию при принятом тесте (см. Succeed);
  • TesterFail - сообщение, выводимое чекером при преждевременном завершении player'а (при ошибке);
  • BadIO - сообщение о некорректном вводе, предназначенное для использования с RageExit;
  • OutOfRange - сообщение о некорректном значении введённой величины, предназначенное для использования с RageExit.

Также библиотека DL_I предоставляет следующие процедуры:

  • Procedure Init(OutFileName : AnsiString) - процедура инициализации библиотеки, должна быть запущена самой первой (параметр - имя выходного файла player'а);
  • Procedure RageExit(ErrMsg : AnsiString) - процедура, выводящая в выходной файл пометку о непройденном тесте и сообщение ErrMsg, которое чекер выводит в комментарии (туда можно поместить информацию о причине отрицательного результата);
  • Procedure Succeed(Comment : AnsiString = Success) - процедура, выводящая в выходной файл пометку о пройденном тесте и сообщение Comment, по умолчанию равное Success, которое чекер выводит в комментарии (туда можно поместить дополнительную информацию о результате).

Важные моменты. Стандартный чекер для интерактивных задач не позволяет тестировать интерактивные задачи, в которых существует возможность частичного принятия теста (качественная оценка результата). Также, чекер предназначен для player'ов, делающих вывод в формате DL_I; так что для player'ов, не использующих эту библиотеку (в частности, сторонних) он неприменим. Поэтому в подобных случаях приходится реализовывать другой чекер (впрочем, проще всего сделать модификацию этого); файл вывода player'а должен содержать информацию, на основе которой чекер сможет определить результат тестирования (опять же, за основу можно взять функции из кода DL_I).

Исходный код библиотеки и чекера содержится в приложениях Г и Д соответственно.

Примеры. Пример player'а, написанного с использованием библиотеки DL_I, результат работы которого можно обрабатывать стандартным чекером для интерактивных задач, содержится в приложении Е.

В этом примере тестируемая программа должна угадать натуральное число в заданных пределах; player выводит «H», если искомое число больше предлагаемого, «L» - если меньше, «OK» - если программа угадала (в этом случае ей надлежит завершить работу).

Заключение

На данный момент все разработки, которые были представлены в данной курсовой работе, внедрены и используются на сайте «Дистанционного обучения».

Система находится в постоянном развитии, в ней постепенно добавляются новые функции и возможности, а заодно поддерживается весь старый функционал.

Программа конвертации задач с групповыми тестами позволяет автоматизировать установку задач по программированию с олимпиад, на которых практиковалось групповое тестирование, а несложная система конфигурации позволяет без особых проблем добавлять в базу известных форматов новые типы задач.

Программа тестирования интерактивных задач с настраиваемым поведением позволяет тестировать более широкий спектр интерактивных задач, а вспомогательная библиотека и чекер сокращают процесс подготовки программ тестирования этих задач.

Подводя итог, можно заключить, что разработанные инструменты позволят сократить время и усилия, затрачиваемые на установку новых задач по программированию в систему «Дистанционного обучения».

Список использованных источников

1. Краткий формат файла task.cfg / Документация системы «Дистанционного обучения в Беларуси» [эл. ресурс]. - URL: http://dl.gsu.by/doc/use/taskcfg.htm. - Дата доступа: 28.03.2012.

2. Установка задач по программированию / Документация системы «Дистанционного обучения в Беларуси» [эл. ресурс]. - URL: http://dl.gsu.by/doc/use/programmers_task.htm. - Дата доступа: 29.03.2012.

3. Установка нового проверяющего модуля для интерактивной задачи / Документация системы «Дистанционного обучения в Беларуси» [эл. ресурс]. - URL: http://dl.gsu.by/doc/use/interactive.htm. - Дата доступа: 14.07.2012.

4. Установка интерактивных задач / Документация системы «Дистанционного обучения в Беларуси» [эл. ресурс]. - URL: http://dl.gsu.by/doc/use/interactive.html. - Дата доступа: 15.07.2012.

5. Free Pascal Online Documentation / Free Pascal official website [electronic source]. - URL: http://www.freepascal.org/docs.var. - Access date: 25.03.2012

6. Azeem M.A. Start programming using Object Pascal Language / Ed. by Anderson P., Hackney J. - code.sd, 2011 - 150 p.

Приложение А. Исходный код приложения ZeroInc

ZeroInc.pas
{$I-}
Uses
  SysUtils;

CONST
  In_  = '';
  _In  = '.in';
  Out_ = '';
  _Out = '.out';
  _Bak = '.bak';

CONST
  MaxN = 1000;

VAR
  Num       : Array [1..MaxN] Of LongWord;
  StartDir  : AnsiString;
  N         : LongWord;

  Function Init (DirName : AnsiString) : Boolean;
  Var
    FName : TSearchRec;
    S     : AnsiString;
    U     : LongWord;
    Code  : Integer;

  Begin
    Init:=False;
    ChDir(StartDir);
    ChDir(DirName);
    If (IOResult <> 0) Then Begin
      {$IfNDef NoLog}
      WriteLn('Can''t go into directory "'+DirName+'"!');
      {$EndIf}
      Exit;
    End;
    If (Not FileExists(In_+'0'+_In)) Then Begin
      {$IfNDef NoLog}
      WriteLn('Directory "'+DirName+'" doesn''t contain the file "'+In_+'0'+_In+'"!');
      {$EndIf}
      Exit;
    End;
    N:=0;
    WriteLn(In_+'*'+_In+':');
    If (FindFirst(In_+'*'+_In, faAnyFile And faDirectory, FName) = 0)
      Then Repeat
        Write(FName.Name+' ');
        With (FName)
          Do S:=Copy(Name, Length(In_), Length(Name)-Length(In_+_In));
        Val(S, U, Code);
        WriteLn(S);
        If (Code <> 0)
          Then Continue;
        Inc(N);
        Num[N]:=U;
      Until (FindNext(FName) <> 0);
    FindClose(FName);
    {$IfNDef NoLog}
    Write(N, ' files found in directory "'+DirName+'". Renaming...');
    {$EndIf}
    Init:=True;
  End;

  Procedure Body ();
  Var
    NewNums     : Array [1..MaxN] Of String[10];
    S           : AnsiString;
    i           : LongWord;

  Begin
    For i:=1 To N Do Begin
      Str(Num[i], S);
      Str(Num[i]+1, NewNums[i]);
      RenameFile(In_+S+_In, In_+NewNums[i]+_In+_Bak);
      If (FileExists(Out_+S+_Out))
        Then RenameFile(Out_+S+_Out, Out_+NewNums[i]+_Out+_Bak);
    End;
    For i:=1 To N Do Begin
      RenameFile(In_+NewNums[i]+_In+_Bak, In_+NewNums[i]+_In);
      If (FileExists( Out_+NewNums[i]+_Out+_Bak ))
        Then RenameFile(Out_+NewNums[i]+_Out+_Bak, Out_+NewNums[i]+_Out);
    End;
    {$IfNDef NoLog}
    WriteLn(' Done!');
    {$EndIf}
  End;

VAR
  _ : LongWord;

BEGIN
  GetDir(0, StartDir);   // default is work directory
  If (ParamCount = 0)
    Then If ( Init(StartDir) )
      Then Body();
  For _:=1 To ParamCount
    Do If (Init( ParamStr(_) ))
      Then Body();
END.

Приложение Б. Исходный код функции проверки принадлежности имени файла шаблону в приложении TaskConv

TaskConv.pas: Procedure ListFiles
// generate infile/outfile list
Procedure ListFiles (Pattern : TFilePattern;  Var Files  : Array Of TTestFile);
Var
  S, Z,
  S1, Z1  : AnsiString;
  i       : Word;

  // recursive add
  Procedure AddRec (Found, Pat  : AnsiString;  Sub  : Word);
  Var
    Info   : TSearchRec;

  Begin
    If (Pos(Sl, Pat) = 0) Then Begin
      // adding files
      If (FindFirst(Found+Pat, faAnyFile, Info) = 0)
        Then Repeat
          If (DirectoryExists(Found+Info.Name))
          Then Continue;
//        WriteLn(Found+Info.Name);
        Files[i].Name:=Found+Info.Name;
        Files[i].SubTest:=Sub;
        Files[i].Valid:=True;
        Inc(i) ;
      Until (FindNext(Info) <> 0);
    End Else Begin
      // going through subdirectories
      If (FindFirst(Found+Copy(Pat,1,Pos(Sl,Pat)-1), faAnyFile, Info) = 0)
        Then Repeat
          If (Info.Name = '.') Or (Info.Name = '..')
            Then Continue;
          If (Not DirectoryExists(Found+Info.Name))
            Then Continue;
          AddRec( Found+Info.Name+Sl, Copy(Pat, Pos(Sl, Pat)+1, Length(Pat)), Sub );
        Until (FindNext(Info) <> 0);
    End;
    FindClose(Info);
  End;

  // extract number from beginning of the string (S, SS)
  Procedure RmNBeg (Pat  : String;  Var Num  : Word;  Var Valid  : Boolean);
  Begin
    Num:=0;
    Valid:=False;
    While (Length(Z) > 0)
      Do If (Pos(Z[1], Digits) > 0)
        Then Begin
          Valid:=True;
          Num:=Num*10+(Pos(Z\[1\], Digits)-1);
          Delete(Z, 1, 1);
        End Else Break;
    Delete(Z1, 1, Length(Pat));
  End;

  // extract number from end of the string (S, SS)
  Procedure RmNEnd (Pat  : String;  Var Num  : Word;  Var Valid  : Boolean);
  Var
    D     : Word = 1;

  Begin
    Num:=0;
    Valid:=False;
    While (Length(Z) > 0)
      Do If (Pos(Z[Length(Z)], Digits) > 0)
        Then Begin
          Num:=Num+D*(Pos(Z[Length(Z)], Digits)-1);
          D:=D*10;
          Valid:=True;
          Delete(Z, Length(Z), 1);
        End Else Break;
    Delete(Z1, Length(Z1)-Length(Pat)+1, Length(Pat));
  End;

  // extract character from beginning of the string (SL)
  Procedure RmCBeg (Pat  : String;  Var Num  : Word;  Var Valid  : Boolean);
  Begin
    Num:=Pos(Z[1], Letters);
    Delete(Z, 1, 1);
    Delete(Z1, 1, Length(Pat));
    Valid:=(Num > 0);
  End;

  // extract character from end of the string (SL)
  Procedure RmCEnd (Pat  : String;  Var Num  : Word;  Var Valid  : Boolean);
  Begin
    Num:=Pos(Z[Length(Z)], Letters);
    Delete(Z, Length(Z), 1);
    Delete(Z1, Length(Z1)-Length(Pat)+1, Length(Pat));
    Valid:=(Num > 0);
  End;

  // extract word from beginning of the string (TaskName)
  Procedure RmSBeg (Pat  : String;  Var Valid  : Boolean);
  Begin
    Valid:=False;
    While (Length(Z) > 0)
      Do If (Pos(Z[1], ABC) > 0) Then Begin
        Delete(Z, 1, 1);
        Valid:=True;
      End Else Break;
    Delete(Z1, 1, Length(Pat));
  End;

Begin
  i:=0;
  // generate list
  AddRec(TaskDir.Value, Pattern.SearchPattern, 0);
  If (Pattern.SearchPattern1 <> '')
    Then AddRec(TaskDir.Value, Pattern.SearchPattern1, 1);

  // going through list, calculating numbers and validating
  For i:=0 To High(Files)
    Do With (Files\[i\]) Do Begin
      S:=Name+Sl;
      S1:=TaskDir.Value+Pattern.BasePattern+Sl;
//      WriteLn(S+' '+S1);
      While (Length(S) > 0) And (Valid) Do Begin
        // splitting both paths into directories to avoid misdetection
        Z:=AnsiLowerCase( Copy(S, 1, Pos(Sl, S)-1) );
        Delete(S, 1, Pos(Sl, S));
        Z1:=AnsiLowerCase( Copy(S1, 1, Pos(Sl, S1)-1) );
        Delete(S1, 1, Pos(Sl, S1));
        While (Length(Z) > 0) And (Valid) Do Begin
//          WriteLn(Z, ' ', Z1);
          // removing regular characters
          If (Z = Z1)
            Then Break;
          While (Z[1] = Z1[1]) Do Begin
            Delete(Z, 1, 1);                      // from beginning
            Delete(Z1, 1, 1);
          End;
          While (Z[Length(Z)] = Z1[Length(Z1)]) Do Begin
            Delete(Z, Length(Z), 1);              // and from the end
            Delete(Z1, Length(Z1), 1);
          End;

          // removing $[ss] $[sl] from the beginning
          If (Pos('$[ss]', Z1) = 1) Or (Pos('$[sl]', Z1) = 1) Then Begin
            If (SubTest = 0)                      // checking if 0th or 1st search pattern was used
              Then If (Z1[4] = 's')
                Then RmNBeg('$[ss]', SubTest, Valid)
                Else RmCBeg('$[sl]', SubTest, Valid)
              Else Delete(Z1, 1, Length('$[sl]'));
            Continue;
          End;

          // removing $[ss] $[sl] from the end
          If (Length('$[ss]') <= Length(Z1)) And ((Pos('$[sl]', Z1)+Length('$[ss]') = Length(Z1)+1)
              Or (Pos('$[sl]', Z1)+Length('$[sl]') = Length(Z1)+1)) Then Begin
            If (SubTest = 0)                      // checking if 0th or 1st search pattern was used
              Then If (Z1[Length(Z1)-1] = 's')
                Then RmNEnd('$[ss]', SubTest, Valid)
                Else RmCEnd('$[sl]', SubTest, Valid)
              Else Delete(Z1, Length(Z1)-Length('$[ss]')+1, Length('$[sl]'));
            Continue;
          End;

          // removing ${sl} from beginning
          If (Pos('${sl}', Z1) = 1) Then Begin
            RmCBeg('${sl}', SubTest, Valid);
            Continue;
          End;
          // removing ${sl} from the end
          If ( Length('${sl}') <= Length(Z1) )
              And ( Pos('${sl}', Z1) + Length('${sl}') = Length(Z1)+1 ) Then Begin
            RmCEnd('${sl}', SubTest, Valid);
            Continue;
          End;

          // removing ${ss} from beginning
          If (Pos('${ss}', Z1) = 1) Then Begin
            RmNBeg('${ss}', SubTest, Valid);
            Continue;
          End;
          // removing ${ss} from the end
          If ( Length('${ss}') <= Length(Z1) )
              And ( Pos('${ss}', Z1) + Length('${ss}') = Length(Z1)+1 ) Then Begin
            RmNEnd('${ss}', SubTest, Valid);
            Continue;
          End;

          // removing ${s} from beginning
          If (Pos('${s}', Z1) = 1) Then Begin
            RmNBeg('${s}', Test, Valid);
            Continue;
          End;
          // removing ${s} from the end
          If ( Length('${s}') <= Length(Z1) )
              And ( Pos('${s}', Z1) + Length('${s}') = Length(Z1)+1 ) Then Begin
            RmNEnd('${s}', Test, Valid);
            Continue;
          End;

          // removing ${TaskName} (will be processed the last - for sake of avoiding worst detection problems)
          If (TaskName.Specified)
            Then Z1:=AnsiLowerCase(TaskName.Value) + Copy(Z1, Length('${taskname}')+1, Length(Z1))
            Else RmSBeg('${taskname}', Valid);
        End;// Z~Z1 cycle
      End;// S~S1 cycle
    End;// Files[i] cycle
End;

Приложение В. Исходный код приложения StdSwp

{$Mode ObjFPC}
{$Define LOGGING}
{$H+,X+}
{$R-,S-,Q-,I-}
Uses
  Process;

CONST   // config & logging
  CfgFile  = 'stdswp.cfg';
{$IfDef DEBUG}
 {$IfDef LOGGING}
  LogFile  = '';
//  LogFile  = 'logfile.log';
  ABC      = '><';
 {$EndIf}
{$EndIf}
  W        = 12;

CONST
  BlockSize = 255;

TYPE
  PProcess = ^TProcess;
  TOption = Record
    Name, Value : String;
  End;

CONST
  c_Rem     = '#';        // comment
  c_Sep     = '=';        // name c_Sep value

CONST
  OptNum    = 3;
  o_Kill    = 1;
  o_PKill   = 2;
  o_Listen  = 3;

VAR
  Options : Array [1..OptNum] Of TOption = (
    (Name: 'KillSolution';  Value: 'No'),
    (Name: 'KillPlayer';    Value: 'No'),
    (Name: 'ListenError';   Value: 'Yes')
  );

CONST
  p_Player   = 1;
  p_Solution = 2;

VAR
  S, Z          : String;
  Proc          : Array [1..2] Of TProcess;
  Lines         : Array [1..2] Of LongWord;
  Kill          : Array [1..2] Of Boolean;
  P             : LongWord;
  _ExitProc     : Pointer;
  i             : Byte;

  // print error message
  Procedure RageExit (ErrMsg : String);
  Begin
{$IfDef DEBUG}
    WriteLn('Error: '+ErrMsg);
{$EndIf}
    Halt;
  End;

  // terminating processes
  Procedure OnExit;
  Var
    i   : Byte;

  Begin
    ExitProc:=_ExitProc;
    For i:=1 To 2
      Do Proc[i].Terminate(0);
    Close(Output);
  End;

  // init process variables
  Procedure Prepare (Proc  : PProcess;  FileName  : String);
  Begin
    Proc^:=TProcess.Create(NIL);
    Proc^.Executable:=FileName;         // alternatively, CommandLine or ApplicationName can be used [both deprecated]
{
    With (Proc^.Parameters) Do Begin    // used to set command line parameters
      Add();
    End;
}
    Proc^.Options:=[poUsePipes];        // redirecting I/O streams
  End;

  // read from output
  Function GetText (Proc : PProcess;  Var Text  : String;  Var L  : LongWord): Boolean;
  Begin
    GetText:=Proc^.Running;              // returning false if process has terminated
//    WriteLn(Proc^.Executable);
    If (Proc^.Output.NumBytesAvailable = 0)
      Then Exit;
    With (Proc^.Output) Do Begin
      L:=NumBytesAvailable;
      If (L > BlockSize)
        Then L:=BlockSize;
//      WriteLn(L);
      Read(Text[1], L);
    End;
  End;

  // write to input
  Function PutText (Proc  : PProcess;     Text : String;       L : LongWord): Boolean;
  Begin
    PutText:=Proc^.Running;
    If (L = 0)
      Then Exit;
    If (Not Proc^.Running)// can't write if process terminated
      Then Exit;
//    WriteLn(Proc^.Executable);
    Proc^.Input.Write(Text[1], L);
  End;

  // read from stderr
  Function GetErr (Proc  : PProcess): String;
  Var
    Text : String;
    L    : LongWord;
  Begin
    GetErr:='';              // returning false if process has terminated
    With (Proc^.StdErr) Do Begin
      L:=NumBytesAvailable;
      SetLength(Text, L);
//      WriteLn(L);
      Read(Text[1], L);
    End;
    GetErr:=Text;
  End;

  // run 2 processes and pipe their I/O streams
  Procedure Connect (A, B  : Byte);
  Var
    Text : String;
    L    : LongWord;

    // exchange values A&B
    Procedure Swap;
    Var
      X  : Byte;
    Begin
      X:=A;   A:=B;   B:=X;
    End;

    // read output of A and send it to B
    Procedure Tell;
    Begin
      GetText(@(Proc[A]), Text, L);
      If (L = 0)
        Then Exit;
      Inc(Lines[A]);
{$IfDef DEBUG}
 {$IfDef LOGGING}
      Write(' '+ABC[A]+' '+Copy(Text, 1, L));     // logging output
      Flush(Output);
 {$EndIf}
{$EndIf}
{      Write('=');
      Flush(Output);}
      PutText(@(Proc\[B\]), Text, L);
      L:=0;
    End;

  Begin
    _ExitProc:=ExitProc;
    ExitProc:=@OnExit;
    Proc[A].Execute;
    Proc[B].Execute;
    Lines[A]:=0;
    Lines[B]:=0;
{    Text:='0'+LineEnding+'All fine!'+LineEnding;
    PutText(@(Proc[p_Player]), Text, Length(Text));}
    SetLength(Text, BlockSize);
    L:=0;
    While (Proc[A].Running) And (Proc[B].Running) Do Begin
      Tell;
{      WriteLn(',');
      Flush(Output);}
      Swap;
      With (Options[o_Listen])
        Do If (UpCase(Value) = 'YES') And (Proc[p_Player].StdErr.NumBytesAvailable > 0)
          Then RageExit(GetErr( @(Proc[p_Player]) ));
    End;
{    WriteLn(';');
    Flush(Output);}
    If (Proc[A].Running)
      Then Swap;
    While (Proc[A].Output.NumBytesAvailable > 0)
      Do Tell;
    With (Options[o_Listen])
      Do If (UpCase(Value) = 'YES') And (Proc[p_Player].StdErr.NumBytesAvailable > 0)
        Then RageExit(GetErr( @(Proc\[p_Player\]) ));
{    WriteLn('.');
    Flush(Output);}
  End;

BEGIN
//  {$I-}
  Assign(Input, CfgFile);
  Reset(Input);
  If (IOResult = 0) Then Begin
    While (Not EoF) Do Begin
      ReadLn(S);
      P:=Pos(c_Rem, S);
      If (P > 0)
        Then Delete(S, P, Length(S)-P+1);
      P:=Pos(c_Sep, S);
      If (P = 0)
        Then Continue;
      Z:=Copy(S, 1, P-1);
      Delete(S, 1, P);
      For i:=1 To OptNum
        Do With (Options[i])
          Do If (Name = Z)
            Then Value:=S;
    End;
    Close(Input);
  End;

{$IfDef DEBUG}
 {$IfDef LOGGING}
  Assign(Output, LogFile);
  Rewrite(Output);
 {$EndIf}
{$EndIf}
  If (ParamCount <> 2)
    Then RageExit('2 parameters expected!');
{$IfDef DEBUG}
  WriteLn(ParamStr(1)+' '+ParamStr(2));
  Flush(Output);
{$EndIf}
  FillChar(Kill, SizeOf(Kill), False);
  With (Options[o_Kill])
    Do If (UpCase(Value) = 'YES') Then Begin
      Kill[p_Solution]:=True;
{$IfDef DEBUG}
      WriteLn(ParamStr(p_Solution), ' is to be killed');
{$EndIf}
    End;
  With (Options[o_PKill])
    Do If (UpCase(Value) = 'YES') Then Begin
      Kill[p_Player]:=True;
{$IfDef DEBUG}
      WriteLn(ParamStr(p_Solution), ' is to be killed');
{$EndIf}
    End;

  For i:=1 To 2
    Do Prepare( @(Proc[i]), ParamStr(i) );
  Connect(1, 2);

  For i:=1 To 2 Do Begin
{$IfDef DEBUG}
    Write(Proc[i].Executable:W);
    Flush(Output);
{$EndIf}
    If (Proc[i].Running) And (Kill[i]) Then Begin
      Proc[i].Terminate(0);
{$IfDef DEBUG}
      Write(' has been terminated on demand.')
{$EndIf}
    End Else Begin
      Proc[i].WaitOnExit;
{$IfDef DEBUG}
      Write(' has terminated successfully. ')
{$EndIf}
    End;
{$IfDef DEBUG}
    WriteLn(' [', Lines[i]:5, ' blocks read]');
{$EndIf}
    Flush(Output);
  End;
  Close(Output);
  ExitProc:=_ExitProc;
END.

Приложение Г. Исходный код библиотеки DL_I

DL_I.pp
Unit DL_I;
{$Mode ObjFPC}

Interface

  CONST   // results/grades
    Failed      = '0';
    Success     = 'OK!';
    TesterFail  = 'Interactive tester stopped with an error!';
    BadIO       = 'Incorrect input!';
    OutOfRange  = 'Given value out of range!';

  Procedure Init (OutFileName  : AnsiString);
  Procedure RageExit (ErrMsg  : AnsiString);
  Procedure Succeed (Comment  : AnsiString = Success);

Implementation

  VAR
    F          : Text;
    Inited     : Boolean = False;
    OutFile    : AnsiString = '';

  Procedure Init (OutFileName  : AnsiString);
  Begin
    OutFile:=OutFileName;
    Inited:=True;
    Assign(F, OutFile);  Rewrite(F);
    WriteLn(F, Failed);
    WriteLn(F, TesterFail);
    Close(F);
  End;

  Procedure RageExit (ErrMsg  : AnsiString);
  Begin
    If (Not Inited)
      Then Exit;
    Assign(F, OutFile);  Rewrite(F);
    WriteLn(F, Failed);
    WriteLn(F, ErrMsg);
    Close(F);
    WriteLn(StdErr, ErrMsg);
    Halt(1);
  End;

  Procedure Succeed(Comment : AnsiString);
  Begin
    If (Not Inited)
      Then Exit;
    Assign(F, OutFile);  Rewrite(F);
    WriteLn(F, Success);
    WriteLn(F, Comment);
    Close(F);
    Halt;
  End;

END.

Приложение Д. Исходный код стандартного чекера для интерактивных задач

CONST  // resulting file
  ResFile   = '$result$.txt';

CONST  // success check
  Good      = 'OK!';

CONST  // commandline parameters order
  p_InFile  = 1;
  p_ChkFile = 2;
  p_OutFile = 3;
  p_MaxBall = 4;

VAR
  InFile, ChkFile,
  OutFile, S        : AnsiString;
  Max               : Word;  // maximum number of points

  Procedure Pre;
  Var
    Code  : Integer;

  Begin
    InFile:=ParamStr(p_InFile);
    ChkFile:=ParamStr(p_ChkFile);
    OutFile:=ParamStr(p_OutFile);
    Val(ParamStr(p_MaxBall), Max, Code);
  End;

BEGIN
  Pre;
  Assign(Input, OutFile);
  Reset(Input);
  // checking if testing result was success
  ReadLn(S);
  If (S <> Good)
    Then Max:=0;
  // reading comment line
  ReadLn(S);
  Close(Input);
  Assign(Output, ResFile);
  Rewrite(Output);
  WriteLn(Max);
  WriteLn(S);
  Close(Output);
END.

Приложение Е. Пример исходного кода player'а с использованием библиотеки DL_I

{$I-}   // включает режим программной обработки ошибок ввода-вывода
Uses
  DL_I;

CONST   // имена файлов
  InFile  = 'guess.in';
  OutFile = 'guess.out';

CONST   // значения для ввода-вывода
  Greater = 'H';
  Smaller = 'L';
  Confirm = 'OK';

VAR
  Num, Max, X  : LongWord;

BEGIN
  Init(OutFile);
  // читаем из входного файла максимальное и искомое числа
  Assign(F,    InFile);   Reset(F);
  ReadLn(F, Max, Num);
  Close(F);

  // выводим максимальное число и ждём ввода от тестируемой программы
  WriteLn(Max);
  Flush(Output);
  ReadLn(X);
  // на входе не целое число либо число вне диапазона LongWord, прерываем тестирование
  If (IOResult <> 0)
    Then RageExit(BadIO);
  // пока не дан правильный ответ
  While (X <> Num) Do Begin
    // введённое число вне заданного диапазона (1..Max), прерываем тестирование
    If (X < 1) Or (Max < X)
      Then RageExit(OutOfRange);
    // даём подсказку
    If (X > Num)
      Then Writeln('H')
      Else Writeln('L');
    // принудительно выводим содержимое буфера вывода и ждём ввода
    Flush(Output);
    ReadLn(X);
    // на входе не целое число либо число вне диапазона LongWord, прерываем тестирование
    If (IOResult <> 0)
      Then RageExit(BadIO);
  End;
  // подтверждаем получение правильного ответа
  WriteLn(Confirm);
  // делаем запись об успешном завершении тестирования и завершаем выполнение
  Succeed;
END.
Powered by Atlassian Confluence, the Enterprise Wiki. (Version: http://www.atlassian.com/software/confluence Build:#2.6.1 916) - Ошибка/новая особенность - Свяжитесь с Администраторами