Представлена структурная схема реализованного типового решения, а также подробно описаны базовые классы предметной области, которые были выделены при реализации системы.
Литература
1. Дейт К. Дж. Введение в системы баз данных, 7-е издание. : Пер. с англ. - М. : Издательский дом «Вильямс», 2001. - 1072 с. : ил. - Парал. тит. англ.
2. Mistry R., Misner S. Introducing Microsoft SQL Server 2008 R2, Microsoft Press, Redmond, Washington, 2010, 216 p.
3. Троелсен Э. Язык программирования C# 2010 и платформа .NET 4.0. 5-е издание, Пер. с англ. - М.: Издательский дом "Вильямс", 2011. - 1392 с.: ил. - Парал. тит. англ.
4. Фаулер М. Архитектура корпоративных программных приложений, Пер. с англ. - М.: Издательский дом "Вильямс", 2004. - 544 с.: ил. - Парал. тит. англ.
5. Ambler S.W. Agile Database Techniques—Effective Strategies for the Agile Software, John Wiley & Sons, 2003, 373p.
УДК 004.588 +004.031.42
СИСТЕМА КОНТРОЛЯ ЛАБОРАТОРНЫХ РАБОТ
Лаптев Валерий Викторович, к.т.н., доцент, Астраханский государственный технический университет, Россия, Астрахань, [email protected] Морозов Александр Васильевич, к.т.н., доцент, Астраханский государственный технический
университет, Россия, Астрахань
Мамлеева Аделя Рифкатовна, ассистент, Астраханский государственный технический университет,
Россия, Астрахань
Введение
В работе [1] одним из авторов сформулированы требования к среде обучения программированию. В частности, среда должна обеспечивать контрольный режим, в котором проверяются и оцениваются результаты выполнения контрольного задания. Было отмечено, что программа студента должна проверяться в два этапа: проверка выполнения программы и проверка структуры текста программы.
Основной проблемой при проверке выполнения программы является создание набора тестовых данных. Для всесторонней проверки требуется подготовить не только правильные входные данные, но и неправильные. В большинстве случаев бывает достаточно сложно сделать это вручную. В качестве примера можно привести проверку программы сортировки. Сортировку нужно проверять с помощью следующих тестов:
• пустой массив (неправильные данные);
• массив из одного элемента (неправильные данные);
• массив из 2 элементов (правильные данные);
• неупорядоченный массив (правильные данные);
• упорядоченный массив (правильные данные).
Массивы в двух последних тестах должны быть представлены в трех вариантах: минимального размера (3-10 элементов), среднего размера (десятки-сотни элементов), и массив предельно допустимого размера (тысячи-десятки тысяч элементов). В последнем варианте весьма сложно создать массив вручную. Кроме того, в этом случае трудно удостовериться, что тест выполнен правильно. Поэтому для выполнения подобных проверок требуется автоматизировать процесс подготовки тестовых данных.
Основной принцип проверки выполнения состоит в сравнении выходных данных программы с эталонными данными. Формализация и автоматизация проверки выполнения требуют некоторой стандартизации в оформлении программы. Обычный подход состоит в
92
том, что входные данные программа должна читать из текстового файла с именем input.txt, а результаты записывать в файл с именем output.txt. Выходной файл сравнивается с эталонным и принимается решение о правильности или неправильности проверяемой программы. Подобные системы широко используются в олимпиадном программировании, например, при проведении чемпионатов мира среди команд вузов. Одной из самых известных систем, используемых для проведения соревнований по программированию, является ejudge [2]. В работе [3] программы этого типа названы «контестерами» (от английского слова contest -соревнование). Основное их достоинство - существенное ускорение проверки программы. Однако основная проблема состоит в подготовке эталонного файла.
Второй этап проверки состоит в том, чтобы сравнить структуру программы студента со структурой эталонной программы, записанной в системе преподавателем. Заметим, что проверки подобного типа осуществляется преподавателем не только в дисциплинах по программированию. Например, при изучении дисциплины «Технология программирования» студент должен выполнить лабораторные работы по построению различных диаграмм на графическом языке UML. При изучении дисциплины «Базы данных» студенту необходимо научиться проектировать структуру базы данных и строить диаграммы «Сущность-связь», отображающие эту структуру. Преподаватель сравнивает построенные студентом диаграммы с эталонным ответом и на основании сравнения оценивает деятельность студента. Таким образом, необходим некоторый универсальный инструмент, с помощью которого преподаватель сможет автоматизировать проверку структуры студенческого решения путем сравнения с эталоном, записанным в системе.
В данной работе рассматривается архитектура и конкретная реализация системы контроля, обеспечивающей автоматизацию подготовки обоих типов проверок и непосредственно проверку выполнения контрольного задания студентом.
1. Физическая архитектура системы контроля
Система контроля состоит из четырех узлов. Основными являются три классических узла трехзвенной архитектуры: тонкий клиент, web-сервер и СУБД. Дополнительным узлом является сервер проверки (рис. 1).
Рис. 1 - Физическая архитектура системы
93
Основная часть разработанной системы реализована в виде WSP-пакета на сервере Sharepoint 2010, работающего под управлением Windows Server 2008 с предустановленным веб-сервером Internet Information Server 7. Пользователь (преподаватель/студент) работает с системой через web-браузер. Доступ к web-серверу осуществляется по протоколу HTTP или HTTPS.
Хранение контента (программы студентов, отосланные на проверку, тестовые наборы данных, идентификационные данные студента и заданий) осуществляется на сервере баз данных с установленным Microsoft SQL Server 2008 под управлением операционной системы Windows Server 2008. Запрос данных с сервера осуществляется с использованием стандартного API Sharepoint Server и инструментария ADO.NET.
Проверка лабораторных работ осуществляется на специализированных серверах проверки. Количество серверов проверки и назначение каждого из них не ограничиваются. Сервер проверки периодически опрашивает web-сервер с целью получения новых решений, которые необходимо проверить. Взаимодействие основного узла системы с серверами проверки осуществляется с использованием технологии WCF. Такой подход, с одной стороны упрощает интеграцию систем проверки, а с другой - позволяет гибко настраивать расположение сервера проверки в любом месте локальной сети или даже в Internet (при наличии статического IP- адреса у сервера проверки или при использовании Dynamic DNS).
Подобная архитектура обеспечивает надежную работу системы при исполнении произвольного кода, присылаемого студентами на проверку - отказ сервера проверки не отразится на функционировании всей системы в целом. Кроме того, при таком подходе в дальнейшем становится возможным использовать сервера проверки, работающие, например, на платформе Linux. На текущий момент реализовано два сервера проверки: для проверки лабораторных работ по дисциплине «Программирование на языке высокого уровня» и по дисциплине «Технология программирования».
2. Архитектура сервера проверки
Исходный код программы, подготовленный студентом, является входными данными для проверяющей системы. Каждая программа представлена в системе объектом Solution (Решение), с которым и осуществляются все действия. Сама проверка состоит из двух этапов: предварительного и основного. На первом этапе выполняется подготовка решения к выполнению. В самом простом случае этот этап представляет собой компиляцию программы в исполняемый файл, или просто сохранение исходного текста в виде файла на жестком диске (для интерпретируемых языков). Главным результатом этапа подготовки является возможность тестировать решение студента.
Далее осуществляется валидация решения на одном или нескольких тестах. Каждый тест представлен в системе объектом типа TestTask, который включает входные и выходные параметры, лимит памяти (memory limit) и ограничения по времени выполнения (time limit). Под валидацией в данной работе понимается применение к подготовленному на предыдущем этапе решению студента некоторой формальной вычислительной процедуры V(I,O), где I -набор параметров для генерации тестовых исходных данных, на которых будет осуществляться валидация решения, O - набор параметров для проверки корректности результата, выданного решением студента.
Архитектура системы проверки основана на применении паттернов Template Method (Шаблонный метод) и Strategy (Стратегия) [4]. Для реализации описанных выше действий в системе определены три интерфейса. В интерфейсе IPreparator объявлен один метод Prepare(), который должен реализовать класс-наследник. Этот метод обеспечивает подготовку решения к выполнению. В интерфейсе IRunner объявлен единственный метод Run(), который должен реализовать класс-наследник. Метод обеспечивает запуск подготовленной программы на выполнение. Непосредственно валидация выполняется методом Validate(), который объявлен в интерфейсе IValidate и который должен быть реализован в классе-наследнике. В классе-валидаторе требуется определить свойства InputParams, OutputParams, TimeLimit, MemoryLimit,
94
названия которых говорят сами за себя. Запуск всех нужных методов осуществляется системой в соответствии с паттерном Template Method.
Ссылка на валидатор присутствует в каждом объекте типа TestTask. Отметим необычайную гибкость предложенного подхода. Во-первых, с одной стороны, единственный тестовый объект может быть использован для тестирования любой программы, реализующей алгоритм задания. С другой стороны, для одного задания преподаватель может создать несколько тестов, задав для каждого теста входные и выходные параметры, ограничения по памяти и времени и имя валидатора - количество тестов для задания ограничивается только объемом базы данных на диске. Все тесты для одного задания могут использовать единственный валидатор, но можно для каждого теста реализовать собственный валидатор. Количество и разнообразие валидаторов не ограничивается - расширение системы новыми валидаторами обеспечивает паттерн Strategy.
Во-вторых, система проверки не зависит от языка программирования, и позволяет проверять программы студентов на любых языках. Это определяется реализацией в классах-наследниках от интерфейсов IPreparator и IRunner методов Prepare() и Run() соответственно (ниже показан пример того, как это сделано для C++).
Более того, механизм IPreparator+ IValidate позволяет реализовать серверы проверки не только для исполняемых программ. В частности, в рамках данной работы реализован простой валидатор для проверки диаграммы классов, работающий по принципу сравнения реализованного студентом решения с эталонным решением, записанным в системе (см. ниже). Этот механизм позволяет реализовать и проверку структуры программы. На этапе подготовки по тексту программы можно построить необходимые модели, а валидатор выполнит проверку моделей, сравнение с эталонной моделью. Можно осуществлять более сложную работу: поиск плагиата, оценивание качества решений [5].
3. Примеры реализации
В качестве примера рассмотрим тексты реализации всех методов для простейшей проверки программы на C++. Реализация выполнена на C#. Для компиляции С++-программ использовался штатный компилятор среды Visual Studio 2010.
// -- Подготовительный класс для C++
public class CppPreparator: IPreparator {
private readonly DirectoryInfo testingTemp = // Директория временных файлов new DirectoryInfo(Environment.GetEnvironmentVariable("TEMP"))
.CreateSubdirectory("Nippel");
// -- Компиляция исходного кода
public IRunner Prepare(CheckerServiceInterfaces.SolutiononeSourceFileSolution,
out string message)
{
message = String.Empty;
SaveSourceFile(oneSourceFileSolution); // сохранение исходного текста CompileSolution(); // компиляция if (File.Exists("Solution.exe"))
return new ExeRunner("Solution.exe"); else return null;
}
// -- сохранение исходного текста -private void SaveSourceFile(CheckerServiceInterfaces.Solution solution)
{
var workDir = testingTemp.CreateSubdirectory(Guid.NewGuid().ToString()); Environment.CurrentDirectory = workDir.FullName;
var file = new StreamWriter("solution.cpp"); // название решения
file.Write(solution.Source);
file.Close();
}
// -- компиляция - компилятор из среды Visual Studio 2010 -private void CompileSolution()
95
{
const string cmdLine = @" && ""C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cl.exe"" /EHsc Solution.cpp";
var psi = new ProcessStartInfo( @"""C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\vcvars32.bat""", сmdLine)
{UseShellExecute = false, CreateNoWindow = false, RedirectStandardOutput = true}; var compilerProcess = Process.Start(psi); if (compilerProcess == null)
throw new ApplicationException("Не возможно создать процесс компилятора"); if ((!compilerProcess.HasExited) && (!compilerProcess.WaitForExit(200000)))
{
compilerProcess.Kill();
throw new ApplicationException("Компиляция продолжалась слишком долго.");
}
}
}
}
// -- исполнитель откомпилированной программы
public class ExeRunner : IRunner {
private readonly string exeName;
public ExeRunner(string exeName) { exeName = exeName; }
// -- запуск на выполнение
public ErrorType Run(Solution oneSourceFileSolution, int timeLimit,
int memoryLimit,
byte[] input, out byte[] output, out string message)
{
using (var sw = File.Create("input.txt"))
{
sw.Write(input, 0, input.Length); sw.Close();
}
var psi = new ProcessStartInfo( exeName, " < input.txt > output.txt")
{
ErrorDialog = false, UseShellExecute = false,
RedirectStandardInput = true, RedirectStandardOutput = true, CreateNoWindow = true };
message = String.Empty; output = null;
var testProcess = Process.Start(psi); // -- старт процесса // -- проверки ошибок
if ((!testProcess.HasExited) && (!testProcess.WaitForExit(timeLimit)))
{
testProcess.Kill();
message = "Превышен лимит времени."; return ErrorType.TimeLimit;
}
if (testProcess.ExitCode != 0)
{
message = "Ошибка времени исполнения."; return ErrorType.RuntimeError;
}
using(var sr = File.OpenRead("output.txt"))
{
var size = (int)sr.Length; output = new byte[size]; sr.Read(output, 0, size);
}
return ErrorType.Ok;
}
}
}
// -- класс- простейший валидатор
96
public class SimpleValidator : IValidator {
public string InputParams { get; set; } public string OutputParams { get; set; } public int TimeLimit { get; set; } public int MemoryLimit { get; set; }
public ErrorType Validate(IRunner runner, Solution solution,
ref string message)
{
byte[] output;
var result = runner.Run(solution, TimeLimit, MemoryLimit,
UTF8Encoding.UTF8.GetBytes(InputParams), out output,
out message);
if (result != ErrorType.Ok) return result; var realOutput = UTF8Encoding.UTF8.GetString(output); if (realOutput.Trim() == OutputParams.Trim()// -- проверка на совпадение -return ErrorType.Ok; else
{
message += String.Format("На тесте \n {0} \n ожидался ответ \n {1} \n
а был получен \n {2}.",
InputParams, OutputParams, realOutput); return ErrorType.WrongAnswer;
}
}
}
}
Подготовительный класс CppPreparator и класс-исполнитель ExeRunner могут быть использованы для проверки всех программ на С++. А вот валидатор можно реализовать отдельно для каждой задачи. Например, в системе был реализован отдельный валидатор для проверки программ-сортировок.
class IntegerArraySortValidator : IValidator {
public string InputParams { get; set; } public string OutputParams { get; set; } public int TimeLimit { get; set; } public int MemoryLimit { get; set; }
// -- метод-валидатор -public ErrorType Validate(IRunner runner, Solution solution,
ref string message)
{
var generationParams = InputParams // -- подготовка входных данных --.Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries) .Select(x => Int32.Parse(x)).ToArray(); var seed = generationParams[0]; var count = generationParams[1]; var margin = generationParams[2]; var random = new Random(seed); var inputArray = new int[count]; for (int i = 0; i < count; i++)
inputArray[i] = random.Next(margin + 1); var sb = new StringBuilder(); for (int i = 0; i < count; i++)
sb.Append(inputArray[i]); sb.Append(" "); var input = sb.ToString().Trim(); byte[] output;
// -- запуск тестируемой программы -var result = runner.Run(solution, TimeLimit, MemoryLimit, UTF8Encoding.UTF8
.GetBytes(input), out output, out message); if (result != ErrorType.Ok) return result;
var realOutput = UTF8Encoding.UTF8.GetString(output)
97
.Split(new[] {” ”}
,StringSplitOptions.RemoveEmptyEntries)
.Select(x => Int32.Parse(x)).ToArray(); Array.Sort(inputArray); // -- стандартная сортировка входа -// -- валидация результатов работы программы -if (realOutput.Length != count)
{
message+^'Не правильное количество элементов в выходном массиве.”;
return ErrorType.WrongAnswer;
}
for (int i = 0; i < count; i++)
{
if (realOutput[i] != inputArray[i]) // -- непосредственно проверка --
{
sb = new StringBuilder();
sb.Append(”Ошибка: ожидалась подпоследовательность < ”);
for (int j = Math.Max(0, i - 5);
j<Math.Min(inputArray.Length+1, i + 6); j++) sb.Append(inputArray[i]); sb.Append(” ”); sb.Append(”>, было выведена < ”); for (int j = Math.Max(0, i - 5);
j < Math.Min(inputArray.Length+1, i + 6); j++)
{
sb.Append(realOutput[i]); sb.Append(” ”);
}
sb.Append(”> ”);
message += sb.ToString();
return ErrorType.WrongAnswer;
}
}
return ErrorType.Ok;
}
}
В методе Validate() сначала генерируются входные данные: несортированный массив целых чисел. Затем запускается на выполнение тестируемая программа, которой передается в качестве параметра сгенерированный массив. Результат ее выполнения (предполагается, что это будет отсортированный массив) сравнивается с исходным массивом, который для этого сортируется стандартной сортировкой.
Для использования этого валидатора в системе создается тестовый объект, для которого устанавливаются следующие параметры:
• валидатор: IntegerArraySortValidator;
• входные параметры: 500 1000000 1000000000;
• выходные параметры: пусто;
• ограничение по времени: 500;
• ограничение по памяти: 10000.
Входные параметры задают: стартовое значение генератора случайных чисел, количество элементов генерируемого массива, диапазон генерируемых случайных чисел. Созданный тестовый объект используется для тестирования любых программ сортировки массива целых чисел, реализованных студентами.
И наконец, приведем пример валидатора для диаграммы классов. Диаграммы в системе сохраняются в формате XMI [6]. Принцип работы валидатора состоит в том, чтобы сначала построить из XMI-представления модели для эталонного решения и решения студента, а затем эти модели сравнить. Для этого, кроме непосредственно валидатора, реализованы несколько вспомогательных классов, соответствующих элементам диаграммы классов. Все классы объединяются в единое пространство имен.
namespace UMLValidator {
98
public class UMLValidator : IValidator {
public string InputParams { get; set; } public string OutputParams { get; set; } public int TimeLimit { get; set; } public int MemoryLimit { get; set; }
const string XMI = "http://schema.omg.org/spec/XMI/2-1";
// -- метод сравнения моделей -private string Compare(Model studentmodel, Model etalonmodel)
{
var resultBuilder = new StringBuilder(); foreach (var classDesc in etalonmodel.Classes)
{
var studentClass = studentmodel.GetClassByName(classDesc.Name); if (studentClass == null)
{
resultBuilder.AppendFormat("B решении отсутствует сущность {0}.",
classDesc.Name);
resultBuilder.AppendLine();
}
else
resultBuilder.Append(ComapareClasses(studentClass, classDesc));
}
foreach (var association in etalonmodel.Associations)
{
var fromStudent = studentmodel.GetClassByName(association.From.Name); var toStudent = studentmodel.GetClassByName(association.To.Name); if (fromStudent != null && toStudent != null)
{
if (studentmodel.GetAssociation(fromStudent, toStudent) == null)
{
resultBuilder.AppendFormat("B решении отсутствует ассоциация между
сущностями {0} и {1}.", association.From.Name, association.To.Name); resultBuilder.AppendLine();
}
}
}
return String.IsNullOrEmpty(resultBuilder.ToString())
? "Модель студента подходит под определение эталонной."
: resultBuilder.ToString();
}
// -- сравнение классов диаграммы -private string ComapareClasses(Class studentClass, Class etalonClass)
{
var resultBuilder = new StringBuilder(); foreach (var property in etalonClass.Properties)
{
if (studentClass.GetPropertyByName(property.Name) == null)
{
resultBuilder.AppendFormat("B модели студента в классе {0} отсутствует
свойство {1}.", etalonClass.Name, property.Name);
resultBuilder.AppendLine();
}
}
return resultBuilder.ToString();
}
// -- формирование моделей из XMI-представления -private Model LoadFromXMI(Stream stream)
{
var xdoc = XDocument.Load(new StreamReader(stream)); var model = new Model(); var classNodes =
99
(from node in xdoc.Descendants()
where node.Name == XName.Get("packagedElement")
&& node.Attribute(XName.Get("type", XMI)) != null && node.Attribute(XName.Get("type", XMI)).Value == "uml:Class" select node).ToList();
// Load classes
foreach (var classNode in classNodes)
{
var classDesc = new Class {
Id = classNode.Attribute(XName.Get("id", XMI)).Value, Name = classNode.Attribute("name").Value
};
classDesc.Properties.AddRange((from node in classNode.Descendants()
where node.Name == "ownedAttribute"
&& node.Attribute(XName.Get("type", XMI)) != null
&& node.Attribute(XName.Get("type", XMI)).Value == "uml:Property" && node.Attribute("name") != null select new Property {
Id = node.Attribute(XName.Get("id", XMI)).Value,
Name = node.Attribute("name").Value } ));
model.Classes.Add(classDesc);
}
// add generalization
foreach (var classNode in classNodes)
{
var generalizations = (from node in classNode.Descendants()
where node.Name == "generalization" select node.Attribute("general").Value).ToList(); if (generalizations.Count > 0)
{
var classDesc = model.GetClassById(classNode.Attribute(XName.Get("id",
XMI)).Value);
foreach (var general in generalizations)
classDesc.Generalization.Add(model.GetClassById(general));
}
}
// add associations
var assNodes = (from node in xdoc.Descendants()
where node.Name == XName.Get("packagedElement")
&& node.Attribute(XName.Get("type", XMI)) != null
&& node.Attribute(XName.Get("type", XMI)).Value == "uml:Association" select node).ToList(); foreach (var associationNode in assNodes)
{
var fromNode = (from node in associationNode.Descendants() where node.Name == "ownedEnd" select node).FirstOrDefault(); var toNode = (from node in associationNode.Descendants() where node.Name == "ownedEnd" select node).LastOrDefault(); if (fromNode != null && toNode != null)
{
var typeIdFrom = (from node in fromNode.Descendants() where node.Name == "type"
select node.Attribute(XName.Get("idref", XMI)).Value).First(); var typeIdTo = (from node in toNode.Descendants() where node.Name == "type"
select node.Attribute(XName.Get("idref", XMI)).Value).First(); var association = new Association {
Name = associationNode.Attribute("name") == null
100
<-р II II
: associationNode.Attribute("name").Value, From = model.GetClassById(typeIdFrom),
To = model.GetClassById(typeIdTo)
};
model.Associations.Add(association);
}
}
return model;
}
// -- простой валидатор - сравнение с эталоном на полное совпадение -public ErrorType Validate(IRunner runner, Solution solution
,ref string message)
{
var studentModel = LoadFromXMI(new MemoryStream(Encoding.UTF8
.GetBytes(solution.Source)));
var etalonModel = LoadFromXMI(new MemoryStream(Encoding.UTF8
.GetBytes(OutputParams))); message += Compare(studentModel, etalonModel); return ErrorType.Ok;
}
}
// -- вспомогательные классы -public class Model {
public Model()
{
Classes = new List<Class>();
Associations = new List<Association>();
}
public List<Class> Classes { get; set; }
public List<Association> Associations { get; set; }
public Class GetClassByName(string name)
{
return Classes.Where(x => x.Name == name).FirstOrDefault();
}
public Class GetClassById(string id)
{
return Classes.Where(x => x.Id == id).FirstOrDefault();
}
public Association GetAssociation(Class from, Class to)
{
return Associations.Where(x => (x.From == from && x.To == to)
|| (x.From == to && x.To == from)) .FirstOrDefault();
}
}
public class Class {
public Class()
{
Properties = new List<Property>();
Generalization = new List<Class>();
}
public string Id { get; set; } public string Name { get; set; } public List<Property> Properties { get; set; } public List<Class> Generalization { get; set; } public Property GetPropertyByName(string name)
{
return (from property in Properties where property.Name == name select property).FirstOrDefault();
}
101
}
public class Property {
public string Id { get; set; }
public string Name { get; set; }
}
public class Association {
public Class From { get; set; }
public Class To { get; set; }
public string Name { get; set; }
}
}
Выводы
Описанная в данной статье система проверки лабораторных работ внедрена и используется на кафедре «Автоматизированные системы обработки информации и управления» Астраханского государственного технического университета. Небольшой пока опыт использования показывает существенное сокращение времени рутинной работы преподавателя по проверке выполненных студентами лабораторных работ.
Дальнейшее развитие системы предполагается проводить в направлении реализации разнообразных валидаторов для оценивания выполненных заданий.
Литература
1. Лаптев В.В. Требования к современной обучающей среде по программированию // Объектные
системы-2010 (Зимняя сессия): материалы II Международной научно-практической
конференции. Россия, Ростов-на-Дону, 10-12 ноября 2010 г. / Под общ. Ред. П.П. Олейника. -Ростов-на-Дону, 2010. - с. 104-110.
2. http://ejudge.ru/ - ejudge contest management system
3. Казачкова, А.А. Особенности использования автоматической системы тестирования решений
в обучении программированию / А.А. Казачкова - http://2011.rn'-
образование.рф/section/83/3758/
4. Мартин, Р. Принципы, паттерны и методики гибкой разработки на языке C# / Р. Мартин, М. Мартин - СПб.: Символ-Плюс, 2011. - 768 с.
5. Лаптев, В.В. Метод оценки качественных характеристик объектно-ориентированного, программного продукта / В.В.Лаптев, А.В.Морозов - Научно-технические ведомости СПбГПУ, № 6(69)/2008. Информатика. Телекоммуникации. Управление. - СПб.: Издательство Политехнического университета, 2008.- с.150-156.
6. http://schema.omg.Org/spec/XMI/2.1 - стандарт XMI
102