Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 108 additions & 7 deletions cmscontrib/loaders/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from cms import config
from cms.db import Contest, User, Task, Statement, Dataset, Manager, Testcase
from cms.grading.languagemanager import HEADER_EXTS, LANGUAGES
from cmscommon.crypto import build_password
from cmscontrib import touch
from .base_loader import ContestLoader, TaskLoader, UserLoader, LANGUAGE_MAP
Expand All @@ -44,6 +45,16 @@ def make_timedelta(t):
return timedelta(seconds=t)


def _format_test_path(pattern, test_number):
"""Format a Polygon test path pattern for a one-based test number."""
try:
return pattern % test_number
except (TypeError, ValueError) as error:
raise ValueError(
"Invalid Polygon test path pattern %r: %s" % (pattern, error)
) from error


class PolygonTaskLoader(TaskLoader):
"""Load a task stored using the Codeforces Polygon format.

Expand Down Expand Up @@ -106,6 +117,20 @@ def get_task(self, get_statement=True):
tree = ET.parse(os.path.join(self.path, "problem.xml"))
root = tree.getroot()

# A Polygon interactor speaks the Polygon file/stdio protocol. CMS
# interactive controllers use a different START_SOLUTION protocol and
# cannot run this binary as a normal batch checker or manager.
interactor_paths = (
os.path.join(self.path, "files", "interactor.cpp"),
os.path.join(self.path, "interactor.cpp"),
)
if any(os.path.exists(path) for path in interactor_paths):
logger.critical(
"Polygon interactors are not supported by the Polygon loader "
"(task %s). Convert the interactor to a CMS controller.", name
)
return None

args["name"] = name
args["title"] = str(root.find('names').find("name").attrib['value'])

Expand Down Expand Up @@ -177,6 +202,48 @@ def get_task(self, get_statement=True):
infile_param = judging.attrib['input-file']
outfile_param = judging.attrib['output-file']

# Polygon batch graders are source files compiled together with
# the contestant's submission. CMS expects these as managers
# named grader.<extension> (for example, grader.cpp). Polygon
# packages commonly put them in files/, but accept the package
# root as well.
compilation_param = "alone"
package_files = [self.path, os.path.join(self.path, "files")]
for language in LANGUAGES:
grader_filename = "grader%s" % language.source_extension
grader_src = next(
(os.path.join(directory, grader_filename)
for directory in package_files
if os.path.isfile(os.path.join(directory, grader_filename))),
None)
if grader_src is None:
continue
logger.info("Batch grader found, importing %s", grader_src)
grader_digest = self.file_cacher.put_file_from_path(
grader_src, "Grader for task %s and language %s" %
(name, language.name))
args["managers"][grader_filename] = Manager(
grader_filename, grader_digest)
compilation_param = "grader"

# Graders may include package-provided headers (for example,
# a participant API header). Import only direct files so the
# manager filename remains valid in the compilation sandbox.
for directory in package_files:
if not os.path.isdir(directory):
continue
for filename in os.listdir(directory):
if not any(filename.endswith(extension)
for extension in HEADER_EXTS):
continue
header_src = os.path.join(directory, filename)
if not os.path.isfile(header_src):
continue
header_digest = self.file_cacher.put_file_from_path(
header_src,
"Header manager %s for task %s" % (filename, name))
args["managers"][filename] = Manager(filename, header_digest)

# Checker can be in any of these two locations.
checker_src = os.path.join(self.path, "files", "check.cpp")
if not os.path.exists(checker_src):
Expand Down Expand Up @@ -211,14 +278,37 @@ def get_task(self, get_statement=True):
evaluation_param = "diff"

args["task_type"] = "Batch"
args["task_type_parameters"] = \
["alone", [infile_param, outfile_param], evaluation_param]
args["task_type_parameters"] = [
compilation_param,
[infile_param, outfile_param],
evaluation_param,
]

args["score_type"] = "Sum"
total_value = 100.0
input_value = 0.0

testcases = int(testset.find('test-count').text)
test_count = testset.findtext('test-count')
if test_count is None:
logger.critical(
"Testset %s for task %s has no test-count.",
testset_name, name)
return None
try:
testcases = int(test_count)
except ValueError:
logger.critical(
"Testset %s for task %s has an invalid test-count %r.",
testset_name, name, test_count)
return None

input_pattern = testset.findtext('input-path-pattern')
answer_pattern = testset.findtext('answer-path-pattern')
if input_pattern is None or answer_pattern is None:
logger.critical(
"Testset %s for task %s has no input/answer path pattern.",
testset_name, name)
return None

n_input = testcases
if n_input != 0:
Expand All @@ -228,10 +318,21 @@ def get_task(self, get_statement=True):
args["testcases"] = {}

for i in range(testcases):
infile = os.path.join(self.path, testset_name,
"%02d" % (i + 1))
outfile = os.path.join(self.path, testset_name,
"%02d.a" % (i + 1))
try:
infile = os.path.join(
self.path, _format_test_path(input_pattern, i + 1))
outfile = os.path.join(
self.path, _format_test_path(answer_pattern, i + 1))
except ValueError as error:
logger.critical("%s", error)
return None
missing = [path for path in (infile, outfile)
if not os.path.isfile(path)]
if missing:
logger.critical(
"Testset %s for task %s references missing file(s): %s",
testset_name, name, ", ".join(missing))
return None
if self.dos2unix_found:
os.system('dos2unix -q %s' % (infile, ))
os.system('dos2unix -q %s' % (outfile, ))
Expand Down
82 changes: 41 additions & 41 deletions cmscontrib/loaders/polygon/testlib-cms.patch
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
--- testlib.h.orig 2018-12-03 23:31:33.532335025 +0200
+++ testlib.h 2018-12-04 19:00:42.933212959 +0200
@@ -41,6 +41,22 @@
*
*/
--- testlib.h.orig
+++ testlib.h
@@ -27,6 +27,16 @@

#define VERSION "0.9.45"

+/*
+ * Artem Iglikov
+ * Alexander Kernozhitsky
+ * Andrey Vihrov
+ *
+ * Modifications for Contest Management System (CMS) support:
+ * - Write checker outcome to stdout and message to stderr
+ * - Use special localizable message strings by default
+ * - Adjust checker exit code
+ * - Adjust checker argument order (except in help and comments)
+ * - Add the "CMS" conditional macro for CMS checker format
+ * - Add the "CMS_VERBOSE_FEEDBACK" conditional macro to enable
+ * testlib-style messages
+ * - Interactors are not supported
+ * - Adjust checker exit code and argument order
+ * - Add the CMS conditional macro for checker format
+ * - Add CMS_VERBOSE_FEEDBACK to opt into testlib-style messages
+ * - Interactors remain unsupported by this checker adapter
+ */
+
/* NOTE: This file contains testlib library for C++.
/*
* Mike Mirzayanov
*
* Check, using testlib running format:
@@ -2468,6 +2484,57 @@
@@ -3102,6 +3112,59 @@ NORETURN void InStream::quit(TResult result, const char *msg) {
int pctype = result - _partially;
bool isPartial = false;

Expand All @@ -40,7 +34,7 @@
+# ifndef CMS_VERBOSE_FEEDBACK
+# define CMS_MSG(code, text) "translate:" code "\n"
+# else
+# define CMS_MSG(code, text) text " %s\n", msg
+# define CMS_MSG(code, text) text " %s\n", message.c_str()
+# endif
+
+ if (result == _ok) {
Expand All @@ -56,53 +50,59 @@
+ std::fprintf(stdout, "0.0\n");
+ std::fprintf(stderr, CMS_MSG(CMS_WRONG, "Wrong Output Format"));
+ } else if (result == _points) {
+ std::string stringPoints(removeDoubleTrailingZeroes(
+ format("%.10f", __testlib_points)));
+ char pointsBuffer[64];
+ std::snprintf(pointsBuffer, sizeof(pointsBuffer), "%.10f",
+ __testlib_points);
+ std::string stringPoints(removeDoubleTrailingZeroes(pointsBuffer));
+ std::fprintf(stdout, "%s\n", stringPoints.c_str());
+ std::fprintf(stderr, CMS_MSG(CMS_PARTIAL, "Partial Score"));
+ } else if (result == _unexpected_eof) {
+ std::fprintf(stdout, "0.0\n");
+ std::fprintf(stderr, CMS_MSG(CMS_WRONG, "Unexpected EOF"));
+ } else if (result >= _partially) {
+ double score = (double)pctype / 200.0;
+ double score = (double) pctype / 200.0;
+ std::fprintf(stdout, "%.3f\n", score);
+ std::fprintf(stderr, CMS_MSG(CMS_PARTIAL, "Partial Score"));
+ } else if (result == _fail) {
+ std::fprintf(stderr, "FAIL %s\n", msg);
+ std::fprintf(stderr, "FAIL %s\n", message.c_str());
+ halt(1);
+ } else {
+ std::fprintf(stderr, "FAIL unknown result %d\n", (int)result);
+ std::fprintf(stderr, "FAIL unknown result %d\n", (int) result);
+ halt(1);
+ }
+
+ halt(0);
+#endif
+
switch (result)
{
case _ok:
@@ -3922,6 +3989,10 @@
switch (result) {
case _ok:
errorName = "ok ";
@@ -4667,6 +4730,12 @@ void setAppesModeEncoding(std::string appesModeEncoding) {
}

void registerInteraction(int argc, char* argv[])
{
void registerInteraction(int argc, char *argv[]) {
+#ifdef CMS
+ quit(_fail, "Interactors are not supported");
+ TestlibFinalizeGuard::registered = true;
+ std::fprintf(stderr, "FAIL Interactors are not supported\n");
+ halt(1);
+#endif
+
__testlib_ensuresPreconditions();
__testlib_set_testset_and_group(argc, argv);
TestlibFinalizeGuard::registered = true;
@@ -4899,9 +4968,14 @@ void registerTestlibCmd(int argc, char *argv[]) {
#endif

testlibMode = _interactor;
@@ -4081,8 +4152,13 @@
}

inf.init(argv[1], _input);
inf.init(args[1], _input);
+#ifdef CMS
+ ouf.init(argv[3], _output);
+ ans.init(argv[2], _answer);
+ ouf.init(args[3], _output);
+ ans.init(args[2], _answer);
+#else
ouf.init(argv[2], _output);
ans.init(argv[3], _answer);
ouf.init(args[2], _output);
- ouf.skipBom();
ans.init(args[3], _answer);
+#endif
+ ouf.skipBom();
}

void registerTestlib(int argc, ...)
void registerTestlib(int argc, ...) {
Loading
Loading