diff --git a/checker.cpp b/checker.cpp index f5dd29a..83f9bd2 100644 --- a/checker.cpp +++ b/checker.cpp @@ -15,31 +15,42 @@ #include "checker.h" #include -#include +#include #include #include - +#include #include "optionsAliases.h" #include "htmlTemplates.h" const int BACKGROUND_TIMELIMIT = 20 * 1000; const int MAX_VISIBLE_THREADS = 2; +const QString TEMP_POSTFIX = "tmp_patched_qrs"; + +#ifdef Q_OS_LINUX +void gnomeEnvironmentHandler(QProcess &process) +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QString xdgCurrentDesktop = env.value("XDG_CURRENT_DESKTOP"); + if (xdgCurrentDesktop.contains("GNOME")) { + QStringList environment; + environment << "QT_QPA_PLATFORM=xcb"; + environment << "QT_DEBUG_PLUGINS=1"; + environment << "DISPLAY=:0"; + process.setEnvironment(environment); + } +} +#endif Checker::Checker(const QString &tasksPath) - : mTasksPath(tasksPath) + : mTasksPath(tasksPath) { } -void Checker::revieweTasks(const QFileInfoList &qrsInfos, const QFileInfoList &fieldsInfos, const QHash &options) +void Checker::reviewTasks(const QFileInfoList &qrsInfos, const QFileInfoList &fieldsInfos, const QHash &options) { - auto patcherOptions = generatePathcerOptions(options); + auto patcherOptions = generatePatcherOptions(options); auto runnerOptions = generateRunnerOptions(options); - QList tasksList; - for (auto &&qrs : qrsInfos) { - tasksList += new Task({qrs, fieldsInfos, patcherOptions, runnerOptions}); - } - QProgressDialog dialog; dialog.setCancelButtonText(tr("Cancel")); dialog.setWindowTitle("TRIK CheckApp"); @@ -48,17 +59,28 @@ void Checker::revieweTasks(const QFileInfoList &qrsInfos, const QFileInfoList &f QFutureWatcher>> watcher; connect(&dialog, &QProgressDialog::canceled, &watcher - , &QFutureWatcher>>::cancel); + , &QFutureWatcher>>::cancel); connect(&watcher, &QFutureWatcher>>::progressRangeChanged - , &dialog, &QProgressDialog::setRange); + , &dialog, &QProgressDialog::setRange); connect(&watcher, &QFutureWatcher>>::progressValueChanged - , &dialog, &QProgressDialog::setValue); + , &dialog, &QProgressDialog::setValue); connect(&watcher, &QFutureWatcher>>::finished, - this, [this, &dialog, &watcher](){ + this, [this, &dialog, &watcher](){ dialog.setLabelText(tr("Creating a report")); if (!watcher.isCanceled()) { - auto result = watcher.result(); - this->createHtmlReport(result); + auto r = watcher.result(); + auto keys = r.keys(); + for (auto &x: keys) { + std::sort(r[x].begin(), r[x].end()); + } + + auto htmlReportName = createHtmlReport(r); + if (!QFile::exists(htmlReportName)) { + qDebug() << "Error: Report file not found: " << htmlReportName; + } + else if (!QDesktopServices::openUrl(QUrl::fromLocalFile(htmlReportName))) { + qDebug() << "Error: Couldn't open url for report file: " << htmlReportName; + } } dialog.reset(); }); @@ -67,6 +89,11 @@ void Checker::revieweTasks(const QFileInfoList &qrsInfos, const QFileInfoList &f QThreadPool::globalInstance()->setMaxThreadCount(MAX_VISIBLE_THREADS); } + QList tasksList; + for (auto &&qrs : qrsInfos) { + tasksList += new Task({qrs, fieldsInfos, patcherOptions, runnerOptions}); + } + createTasksEnvironment(); auto futureTasks = QtConcurrent::mappedReduced(tasksList, checkTask, reduceFunction); watcher.setFuture(futureTasks); @@ -75,158 +102,250 @@ void Checker::revieweTasks(const QFileInfoList &qrsInfos, const QFileInfoList &f watcher.waitForFinished(); } - for(auto &&t : tasksList) { - delete t; - } + removeTasksEnvironment(); + qDeleteAll(tasksList); } -QList Checker::checkTask(const Checker::Task *t) +void Checker::createTasksEnvironment() { - QList result; - QString ext = ""; - if (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows) { - ext = ".exe"; + QDir(mTasksPath).mkdir(TEMP_POSTFIX); +} + +void Checker::removeTasksEnvironment() +{ + const QString tmpDirPath = mTasksPath + QDir::separator() + TEMP_POSTFIX; + QDir(tmpDirPath).removeRecursively(); +} + +QPair Checker::handleJsonReport(const QString &filename) { + + QFile file(filename); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << "Error: The report file could not be opened:" << filename; + return QPair("", "error"); } + const QString jsonString = file.readAll(); + file.close(); + + if (!QFile::remove(filename)) { + qDebug() << "Error: Couldn't delete the report file: " << filename; + } + + QJsonParseError parseError; + const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); + + if (jsonDoc.isNull() || jsonDoc.isEmpty()) { + qDebug() <<"Error: The report file should not be empty " << parseError.errorString(); + return QPair("", "error"); + } + + auto jsonObj = jsonDoc.array().at(0).toObject(); + if (!jsonObj.contains("level") || !jsonObj.contains("message")) { + qDebug() <<"Error: The report file should not be empty " << parseError.errorString(); + return QPair("", "error"); + } + + const QString level = jsonObj["level"].toString(); + const QString message = jsonObj["message"].toString(); + + return QPair(message, level); + } + +Checker::task_results_t Checker::checkTask(const Checker::Task *t) +{ + const QString ext = QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows ? ".exe": ""; + const QString tasksPath = t->qrs.absoluteDir().absolutePath(); + const QString tmpDirPath = tasksPath + QDir::separator() + TEMP_POSTFIX; + + task_results_t result; + + static const QRegularExpression pattern(tr("in") + " (\\d+(\\.\\d+)?) " + tr("sec")); + for (auto &&f : t->fieldsInfos) { - QDir(t->qrs.absoluteDir().absolutePath()).mkdir("tmp"); - const QString patchedQrsName = t->qrs.absoluteDir().absolutePath() + "/tmp/" + t->qrs.fileName(); - QFile(t->qrs.absoluteFilePath()).copy(patchedQrsName); + const QString patchedQrsName = tmpDirPath + QDir::separator() + t->qrs.fileName(); + QFile::copy(t->qrs.absoluteFilePath(), patchedQrsName); QFile patchedQrs(patchedQrsName); - startProcess("patcher" + ext, QStringList(patchedQrs.fileName()) + t->patcherOptions + QStringList(f.absoluteFilePath())); - TaskReport report; report.name = t->qrs.fileName(); report.task = f.fileName(); - report.error = startProcess("2D-model" + ext, QStringList(patchedQrs.fileName()) + t->runnerOptions); report.time = "-"; - if (!isErrorMessage(report.error)) { - int start = report.error.indexOf(tr("in")) + 3; - int end = report.error.indexOf(tr("sec!")) - 1; - if (start - 3 != -1 && end + 1 != -1) { - report.time = report.error.mid(start, end - start); + report.message = executeProcess("./patcher" + ext, QStringList(patchedQrs.fileName()) + t->patcherOptions << f.absoluteFilePath()); + + if (isErrorMessage(report.message)) { + qDebug() << "Error: Failed to patch:" << report.message; + report.level = "error"; + result.append(report); + continue; + } + + const QString reportFileName = tmpDirPath + QDir::separator() + report.name + report.task; + auto additional_options = QStringList("-r") << reportFileName; + + auto message = executeProcess("./2D-model" + ext, QStringList(patchedQrs.fileName()) + t->runnerOptions + additional_options); + + auto twoModelResult = handleJsonReport(reportFileName); + report.level = twoModelResult.second; + + if (twoModelResult.first != "") { + report.message = twoModelResult.first; + + if (twoModelResult.second == "error") { + qDebug() << "Error: Failed to run 2D-model:" << report.message; + result.append(report); + continue; } } + else if (twoModelResult.second == "error") { + report.message = getErrorMessage(message); + qDebug() << "Error: Failed to run 2D-model:" << message; + result.append(report); + continue; + } + + auto match = pattern.match(report.message); + + if (match.hasMatch()) { + report.time = match.captured(1) + " " + tr("sec"); + } + result.append(report); } - QDir(t->qrs.absoluteDir().absolutePath() + "/tmp/").removeRecursively(); return result; } -void Checker::reduceFunction(QHash> &result, const QList &intermediate) +void Checker::reduceFunction(QHash &result, const Checker::task_results_t &intermediate) { - for (auto i : intermediate) { + for (auto &i : intermediate) { result[i.name].append(i); } } -QString Checker::startProcess(const QString &program, const QStringList &options) +QString Checker::executeProcess(const QString &program, const QStringList &options) { - QProcess proccess; - proccess.start(program, options); - if (!proccess.waitForStarted()) { - return "Error: not started"; - } + QProcess process; - if (options.contains("-b") && !proccess.waitForFinished(BACKGROUND_TIMELIMIT)) { - return "Error: not finished"; - } - else { - proccess.waitForFinished(-1); +#ifdef Q_OS_LINUX + gnomeEnvironmentHandler(process); +#endif + + QEventLoop l; + + connect(&process, QOverload::of(&QProcess::finished) + , &l, [&](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitCode) + if (exitStatus == QProcess::ExitStatus::CrashExit) { + l.exit(-1); + } + else { + l.exit(); + } + }); + + connect(&process, &QProcess::errorOccurred, &l, [&](QProcess::ProcessError processError) { + qDebug() << "ERROR" << processError << program << options; + l.exit(-1); + }); + + if (options.contains("-b")) { + QTimer::singleShot(BACKGROUND_TIMELIMIT, Qt::TimerType::CoarseTimer, &l, [&](){ + qDebug() << "ERROR TIMEOUT" << program << options; + l.exit(-2); + }); } - return proccess.readAllStandardError(); -} + process.start(program, options); + auto rc = l.exec(); + switch (rc) { + case -1: return "Error: Application proccess crashed. Please, check manually"; + default: return process.readAllStandardError(); + } + } -void Checker::createHtmlReport(QHash> &result) +const QString Checker::createHtmlReport(const QHash> &result) { auto qrsNames = result.keys(); - auto numberOfCorrect = new int[qrsNames.length()] {0}; std::sort(qrsNames.begin(), qrsNames.end()); - int i = 0; - for (auto &&key : qrsNames) { - std::sort(result[key].begin(), result[key].end(), compareReportsByTask); - foreach (auto r, result[key]) { - numberOfCorrect[i] += isErrorMessage(r.error) ? 0 : 1; - } - i++; - } - - QFile reportFile(mTasksPath + QDir::separator() + "report.html"); - QFile htmlBegin(":/report_begin.html"); - QFile htmlEnd(":/report_end.html"); - - QString body = reportHeader.arg(mTasksPath.section(QDir::separator(), -1)).arg(QDateTime::currentDateTime().toString("hh:mm dd.MM.yyyy")); + const QDateTime dateTime = QDateTime::currentDateTime(); + QString body = reportHeader.arg(mTasksPath.section(QDir::separator(), -1), dateTime.toString("hh:mm:ss dd.MM.yyyy")); - i = 0; + int i = 0; for (auto &&key : qrsNames) { + int numberOfCorrect = 0; auto studentResults = result[key]; + for(auto &&r: studentResults) { + numberOfCorrect += !isErrorReport(r); + } + QString color = yellowCssClass; - if (numberOfCorrect[i] == studentResults.length()) { + if (numberOfCorrect == studentResults.length()) { color = greenCssClass; - } else if (numberOfCorrect[i] == 0) { + } else if (numberOfCorrect == 0) { color = blackCssClass; } - int counter = 0; - QString name; - for (auto &&r : studentResults) { - name = ""; - if (counter == 0) { - name = r.name; - } else if (counter == 1) { - color = ""; - name = QString(tr("Total %1 of %2")).arg(numberOfCorrect[i]).arg(result[key].length()); - } - QString status = isErrorMessage(r.error) ? tr("Error") : tr("Complete"); - body += taskReport.arg(color).arg(name).arg(r.task).arg(status).arg(r.time); + QString name = QString(tr("Total %1 of %2")).arg(numberOfCorrect).arg(studentResults.length()); + body += taskReport.arg(color, key, tr("All"), name, "-"); - counter++; + for (auto &&r : studentResults) { + const QString errorMessage = isErrorReport(r) ? r.message : tr("Complete"); + body += taskReport.arg("", "", r.task, errorMessage, r.time); } + i++; } + QFile htmlBegin(":/report_begin.html"); htmlBegin.open(QFile::ReadOnly); QString report = htmlBegin.readAll(); htmlBegin.close(); report += body; + QFile htmlEnd(":/report_end.html"); htmlEnd.open(QFile::ReadOnly); report += htmlEnd.readAll(); htmlEnd.close(); - std::string html = report.toStdString(); - const auto raw = html.c_str(); - reportFile.open(QIODevice::WriteOnly | QIODevice::Truncate); - reportFile.write(raw); + const QString reportFileName = QString("report_%1.html").arg(dateTime.toString("dd_MM_yyyy_hh_mm_ss")); + QFileInfo reportFileInfo(mTasksPath + QDir::separator() + reportFileName); + QFile reportFile(reportFileInfo.absoluteFilePath()); + if (!reportFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qDebug() << "Error: Failed to open" << reportFileInfo.absoluteFilePath() << "with" << reportFile.errorString(); + } + if (reportFile.write(report.toUtf8()) < 0) { + qDebug() << "Error: Failed to write" << reportFileInfo.absoluteFilePath() << "with" << reportFile.errorString(); + } reportFile.close(); - delete[] numberOfCorrect; + return reportFileInfo.absoluteFilePath(); } const QStringList Checker::generateRunnerOptions(const QHash &options) { QStringList result; - if (options[closeSuccessOption].toBool()) - result << "--close-on-succes"; + if (options[closeSuccessOption].toBool()) { + result << "--close-on-success"; + } - if (options[backgroundOption].toBool()) + if (options[backgroundOption].toBool()) { result << "-b"; + } - if (options[consoleOption].toBool()) + if (options[consoleOption].toBool()) { result << "-c"; + } return result; } -const QStringList Checker::generatePathcerOptions(const QHash &options) +const QStringList Checker::generatePatcherOptions(const QHash &options) { QStringList result; if (options[resetRP].toBool()) { @@ -252,3 +371,19 @@ bool Checker::isErrorMessage(const QString &message) { return message.indexOf(tr("Error")) != -1 or message.indexOf("Error") != -1; } + +bool Checker::isErrorReport(const TaskReport &report) +{ + return report.level == "error"; +} + +QString Checker::getErrorMessage(const QString &message) +{ + auto messageLastIndex = message.lastIndexOf(tr("Error")); + auto endErrorIndex = message.indexOf(QChar::LineFeed, messageLastIndex); + + if (messageLastIndex != -1 and endErrorIndex == -1) { + return message.mid(messageLastIndex); + } + return message.mid(messageLastIndex, endErrorIndex - messageLastIndex + 1); +} diff --git a/checker.h b/checker.h index 217cd2b..275afa2 100644 --- a/checker.h +++ b/checker.h @@ -24,8 +24,8 @@ class Checker : public QObject public: Checker(const QString &tasksPath); - void revieweTasks(const QFileInfoList &qrsInfos, const QFileInfoList &fieldsInfos, const QHash &options); + void reviewTasks(const QFileInfoList &qrsInfos, const QFileInfoList &fieldsInfos, const QHash &options); struct Task { QFileInfo qrs; const QFileInfoList &fieldsInfos; @@ -36,29 +36,39 @@ class Checker : public QObject struct TaskReport { QString name; QString task; - QString error; QString time; + QString message; + QString level; + + bool operator <(const TaskReport& other) const { return task < other.task; } }; private: - static bool compareReportsByTask(const TaskReport &first, const TaskReport &second) - { - return first.task < second.task; - } + typedef QList task_results_t; + + static void reduceFunction(QHash &result, const task_results_t &intermediate); - static void reduceFunction(QHash> &result, const QList &intermediate); + static task_results_t checkTask(const Task *task); - static QList checkTask(const Task *task); + static QString executeProcess(const QString &program, const QStringList &options); - static QString startProcess(const QString &program, const QStringList &options); + static QPair handleJsonReport(const QString &filename); - void createHtmlReport(QHash> &result); + const QString createHtmlReport(const QHash > &result); const QStringList generateRunnerOptions(const QHash &options); - const QStringList generatePathcerOptions(const QHash &options); + const QStringList generatePatcherOptions(const QHash &options); static bool isErrorMessage(const QString &message); + static bool isErrorReport(const TaskReport &message); + + static QString getErrorMessage(const QString &message); + const QString &mTasksPath; + + void createTasksEnvironment(); + + void removeTasksEnvironment(); }; diff --git a/main.cpp b/main.cpp index 8e8d95f..c9eaf3e 100644 --- a/main.cpp +++ b/main.cpp @@ -15,19 +15,25 @@ #include "mainwindow.h" #include -#include #include #include +#include +#include int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication::setStyle(QStyleFactory::create("Fusion")); QApplication a(argc, argv); - a.setStyle(QStyleFactory::create("Fusion")); QTranslator translator; - translator.load(":/translations/checkapp_" + QLocale::system().name() + ".qm"); - a.installTranslator(&translator); + const QString translation_file = ":/translations/checkapp_" + QLocale::system().name() + ".qm"; + if (!translator.load(translation_file)) { + qDebug() << "Failed to load translation file: " << translation_file; + } + else { + a.installTranslator(&translator); + } MainWindow w; w.show(); diff --git a/mainwindow.cpp b/mainwindow.cpp index 00e8cf0..8ccc211 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -25,14 +25,14 @@ #include "optionsAliases.h" MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent) - , mUi(new Ui::MainWindow) - , mTasksDir(QDir::currentPath()) - , mLocalSettings(QDir::toNativeSeparators(mTasksDir.absolutePath() + "/checkapp.ini")) + : QMainWindow(parent) + , mUi(new Ui::MainWindow) + , mTasksDir(QDir::currentPath()) + , mLocalSettings(QDir::toNativeSeparators(mTasksDir.absolutePath() + "/checkapp.ini")) { mUi->setupUi(this); - connect(mUi->backgroundOption, &QGroupBox::toggled, [this](bool state){ + connect(mUi->backgroundOption, &QGroupBox::toggled, this, [this](bool state){ mDirOptions[mTasksPath][backgroundOption] = !state; mUi->closeOnSuccessOption->setEnabled(state); }); @@ -63,9 +63,8 @@ void MainWindow::on_runCheckButton_clicked() showNoFieldsMessage(); return; } - Checker checker(mTasksPath); - checker.revieweTasks(qrsList, fields, mDirOptions[mTasksPath]); + checker.reviewTasks(qrsList, fields, mDirOptions[mTasksPath]); } void MainWindow::on_chooseField_clicked() @@ -136,19 +135,15 @@ QDir MainWindow::chooseDirectoryDialog() return dialog.directory(); } + void MainWindow::resetUiOptions(const QHash &options) { - options[closeSuccessOption].toBool() ? mUi->closeOnSuccessOption->setCheckState(Qt::CheckState::Checked) - : mUi->closeOnSuccessOption->setCheckState(Qt::CheckState::Unchecked); + mUi->closeOnSuccessOption->setCheckState(options[closeSuccessOption].toBool() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); mUi->backgroundOption->setChecked(!options[backgroundOption].toBool()); - options[patchField].toBool() ? mUi->wPPCheckBox->setCheckState(Qt::CheckState::Checked) - : mUi->wPPCheckBox->setCheckState(Qt::CheckState::Unchecked); - options[patchWP].toBool() ? mUi->wPcheckBox->setCheckState(Qt::CheckState::Checked) - : mUi->wPcheckBox->setCheckState(Qt::CheckState::Unchecked); - options[resetRP].toBool() ? mUi->resetPCheckBox->setCheckState(Qt::CheckState::Checked) - : mUi->resetPCheckBox->setCheckState(Qt::CheckState::Unchecked); - options[consoleOption].toBool() ? mUi->resetPCheckBox->setCheckState(Qt::CheckState::Checked) - : mUi->resetPCheckBox->setCheckState(Qt::CheckState::Unchecked); + mUi->wPPCheckBox->setCheckState(options[patchField].toBool() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + mUi->wPcheckBox->setCheckState(options[patchWP].toBool() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + mUi->resetPCheckBox->setCheckState(options[resetRP].toBool() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + mUi->showConsoleCheckBox->setCheckState(options[consoleOption].toBool() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); mUi->xmlFieldsDir->setText(options[xmlFieldsDir].toString()); } @@ -160,7 +155,9 @@ void MainWindow::loadSettings() QHash options; settings.beginGroup(g); - for (auto &&key : defaultOptions.keys()) { + + auto defaultOptionsKeys = defaultOptions.keys(); + for (auto &&key : defaultOptionsKeys) { options[key] = settings.value(key, defaultOptions[key]); } settings.endGroup(); @@ -172,9 +169,11 @@ void MainWindow::loadSettings() void MainWindow::saveSettings() { QSettings settings(mLocalSettings, QSettings::IniFormat); - for (auto &&dir: mDirOptions.keys()) { + auto mDirOptionsKeys = mDirOptions.keys(); + for (auto &&dir: mDirOptionsKeys) { settings.beginGroup(dir); - for (auto &&option: mDirOptions[dir].keys()) { + auto options = mDirOptions[dir].keys(); + for (auto &&option: options) { settings.setValue(option, mDirOptions[dir][option]); } settings.endGroup(); diff --git a/mainwindow.h b/mainwindow.h index 71d8270..9024cc6 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -23,11 +23,11 @@ QT_END_NAMESPACE class MainWindow : public QMainWindow { - Q_OBJECT + Q_OBJECT public: - MainWindow(QWidget *parent = nullptr); - ~MainWindow(); + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); private slots: void on_chooseField_clicked(); diff --git a/optionsAliases.h b/optionsAliases.h index ace32b4..502909c 100644 --- a/optionsAliases.h +++ b/optionsAliases.h @@ -28,9 +28,9 @@ const QString patchWP = "patchWroldAndPosition"; const QString resetRP = "resetRobotPosition"; const QHash defaultOptions {{closeSuccessOption, true} - ,{backgroundOption, false} - ,{consoleOption, false} - ,{xmlFieldsDir, ""} - ,{patchField, true} - ,{patchWP, false}}; + ,{backgroundOption, false} + ,{consoleOption, false} + ,{xmlFieldsDir, ""} + ,{patchField, true} + ,{patchWP, false}}; diff --git a/translations/checkapp_ru.ts b/translations/checkapp_ru.ts index fea37f5..069baf5 100644 --- a/translations/checkapp_ru.ts +++ b/translations/checkapp_ru.ts @@ -4,44 +4,50 @@ Checker - + Cancel Отменить - + A check is performed... Выполняется проверка - + Creating a report Создаю отчёт - + in за сколько секунд за - - sec! - сек! + + + sec + сек - + Total %1 of %2 Итого %1 из %2 - - + + All + Все + + + + Error Ошибка - + Complete Выполнено @@ -49,84 +55,83 @@ MainWindow - + TRIK CheckApp TRIK CheckApp - + <html><head/><body><p align="justify">Select a folder with solutions</p></body></html> - <html><head/><body><p align="justify"><span style=" color:#000000;">Выбрать папку с решениями</span></p></body></html> <html><head/><body><p align="justify">Выбрать папку с решениями</p></body></html> - + Select solutions ... Выбрать решения ... - + Rules for task checking Правила проверки заданий - + <html><head/><body><p>Select the folder with the fields to check</p></body></html> <html><head/><body><p>Выбрать папку с полями для проверки</p></body></html> - + Select fields ... Выбрать поля ... - + Jury selects starting point Точку старта взять с поля проверки - + Set the starting point and the robot configuration Точку старта и конфигурацию робота взять с поля проверки - + Begin at the starting position Начать со стартовой позиции - + Enable check visualization Включить визуализацию проверки - + Display the robot console Отображать консоль робота - + Close the window after checking the task Закрывать окно после проверки задачи - + <html><head/><body><p>Check the solutions on the selected fields</p></body></html> <html><head/><body><p>Проверить решения на выбранных полях</p></body></html> - + Perform a check ... Выполнить проверку ... - + There is no .qrs or .tsj files in solutions directory. В каталоге решений нет файлов .qrs или .tsj. - + There is no .xml files in fields directory. В каталоге полей нет файлов .xml.