From 8c13aa878c13bc93d066069b3dda46851ddda75b Mon Sep 17 00:00:00 2001 From: Luca Versari Date: Wed, 19 Aug 2026 21:49:34 +0200 Subject: [PATCH] Rename stub.%l to grader.%l. Fixes #1705. --- cms/db/__init__.py | 2 +- cms/grading/tasktypes/Communication.py | 48 ++--- cmscontrib/loaders/italy_yaml.py | 27 +-- cmscontrib/loaders/tps.py | 14 +- cmscontrib/updaters/update_49.py | 97 ++++++++++ cmscontrib/updaters/update_from_1.5.sql | 27 +++ .../communication_fifoio_stubbed/__init__.py | 12 +- .../code/{stub.c => grader.c} | 0 .../code/{stub.cpp => grader.cpp} | 0 .../code/{stub.java => grader.java} | 2 +- .../code/{stub.pas => grader.pas} | 2 +- .../code/{stub.py => grader.py} | 0 .../__init__.py | 12 +- .../code/{stub.c => grader.c} | 0 .../code/{stub.cpp => grader.cpp} | 0 .../code/{stub.java => grader.java} | 2 +- .../code/{stub.pas => grader.pas} | 2 +- .../code/{stub.py => grader.py} | 0 .../__init__.py | 12 +- .../code/{stub.c => grader.c} | 0 .../code/{stub.cpp => grader.cpp} | 0 .../code/{stub.java => grader.java} | 2 +- .../code/{stub.pas => grader.pas} | 2 +- .../code/{stub.py => grader.py} | 0 .../communication_stdio_stubbed/__init__.py | 12 +- .../code/{stub.c => grader.c} | 0 .../code/{stub.cpp => grader.cpp} | 0 .../code/{stub.java => grader.java} | 2 +- .../code/{stub.pas => grader.pas} | 2 +- .../code/{stub.py => grader.py} | 0 .../unit_tests/cmscontrib/Update49Test.py | 171 ++++++++++++++++++ .../grading/tasktypes/CommunicationTest.py | 128 ++++++------- docs/External contest formats.rst | 4 +- docs/Task types.rst | 12 +- 34 files changed, 450 insertions(+), 144 deletions(-) create mode 100644 cmscontrib/updaters/update_49.py rename cmstestsuite/tasks/communication_fifoio_stubbed/code/{stub.c => grader.c} (100%) rename cmstestsuite/tasks/communication_fifoio_stubbed/code/{stub.cpp => grader.cpp} (100%) rename cmstestsuite/tasks/communication_fifoio_stubbed/code/{stub.java => grader.java} (97%) rename cmstestsuite/tasks/communication_fifoio_stubbed/code/{stub.pas => grader.pas} (97%) rename cmstestsuite/tasks/communication_fifoio_stubbed/code/{stub.py => grader.py} (100%) rename cmstestsuite/tasks/communication_many_fifoio_stubbed/code/{stub.c => grader.c} (100%) rename cmstestsuite/tasks/communication_many_fifoio_stubbed/code/{stub.cpp => grader.cpp} (100%) rename cmstestsuite/tasks/communication_many_fifoio_stubbed/code/{stub.java => grader.java} (97%) rename cmstestsuite/tasks/communication_many_fifoio_stubbed/code/{stub.pas => grader.pas} (97%) rename cmstestsuite/tasks/communication_many_fifoio_stubbed/code/{stub.py => grader.py} (100%) rename cmstestsuite/tasks/communication_many_stdio_stubbed/code/{stub.c => grader.c} (100%) rename cmstestsuite/tasks/communication_many_stdio_stubbed/code/{stub.cpp => grader.cpp} (100%) rename cmstestsuite/tasks/communication_many_stdio_stubbed/code/{stub.java => grader.java} (96%) rename cmstestsuite/tasks/communication_many_stdio_stubbed/code/{stub.pas => grader.pas} (95%) rename cmstestsuite/tasks/communication_many_stdio_stubbed/code/{stub.py => grader.py} (100%) rename cmstestsuite/tasks/communication_stdio_stubbed/code/{stub.c => grader.c} (100%) rename cmstestsuite/tasks/communication_stdio_stubbed/code/{stub.cpp => grader.cpp} (100%) rename cmstestsuite/tasks/communication_stdio_stubbed/code/{stub.java => grader.java} (95%) rename cmstestsuite/tasks/communication_stdio_stubbed/code/{stub.pas => grader.pas} (93%) rename cmstestsuite/tasks/communication_stdio_stubbed/code/{stub.py => grader.py} (100%) create mode 100644 cmstestsuite/unit_tests/cmscontrib/Update49Test.py diff --git a/cms/db/__init__.py b/cms/db/__init__.py index f26f37facd..71d8e61867 100644 --- a/cms/db/__init__.py +++ b/cms/db/__init__.py @@ -80,7 +80,7 @@ # Instantiate or import these objects. -version = 48 +version = 49 engine = create_engine(config.database.url, echo=config.database.debug, pool_timeout=60, pool_recycle=120) diff --git a/cms/grading/tasktypes/Communication.py b/cms/grading/tasktypes/Communication.py index 9f702c1ae4..25426d16ab 100644 --- a/cms/grading/tasktypes/Communication.py +++ b/cms/grading/tasktypes/Communication.py @@ -57,12 +57,12 @@ class Communication(TaskType): The task type will run *manager*, an admin-provided executable, and one or more instances of the user solution, optionally compiled together with a - language-specific stub. + language-specific grader. During the evaluation, the manager and each of the user processes communicate via FIFOs. The manager will read the input, send it (possibly with some modifications) to the user process(es). The user processes, either - via functions provided by the stub or by themselves, will communicate with + via functions provided by the grader or by themselves, will communicate with the manager. Finally, the manager will decide outcome and text, and print them on stdout and stderr. @@ -82,9 +82,9 @@ class Communication(TaskType): """ # Filename of the manager (the stand-alone, admin-provided program). MANAGER_FILENAME = "manager" - # Basename of the stub, used in the stub filename and as the main class in - # languages that require us to specify it. - STUB_BASENAME = "stub" + # Basename of the grader, used in the grader filename and as the main class + # in languages that require us to specify it. + GRADER_BASENAME = "grader" # Filename of the input in the manager sandbox. The content will be # redirected to stdin, and managers should read from there. INPUT_FILENAME = "input.txt" @@ -94,7 +94,7 @@ class Communication(TaskType): # Constants used in the parameter definition. COMPILATION_ALONE = "alone" - COMPILATION_STUB = "stub" + COMPILATION_GRADER = "grader" USER_IO_STD = "std_io" USER_IO_FIFOS = "fifo_io" @@ -110,7 +110,7 @@ class Communication(TaskType): "compilation", "", {COMPILATION_ALONE: "Submissions are self-sufficient", - COMPILATION_STUB: "Submissions are compiled with a stub"}) + COMPILATION_GRADER: "Submissions are compiled with a grader"}) _USER_IO = ParameterTypeChoice( "User I/O", @@ -137,14 +137,14 @@ def __init__(self, parameters): def get_compilation_commands(self, submission_format): """See TaskType.get_compilation_commands.""" codenames_to_compile = [] - if self._uses_stub(): - codenames_to_compile.append(self.STUB_BASENAME + ".%l") + if self._uses_grader(): + codenames_to_compile.append(self.GRADER_BASENAME + ".%l") codenames_to_compile.extend(submission_format) res = dict() for language in LANGUAGES: source_ext = language.source_extension executable_filename = self._executable_filename(submission_format, - language) + language) res[language.name] = language.get_compilation_commands( [codename.replace(".%l", source_ext) for codename in codenames_to_compile], @@ -153,8 +153,8 @@ def get_compilation_commands(self, submission_format): def get_user_managers(self): """See TaskType.get_user_managers.""" - if self._uses_stub(): - return [self.STUB_BASENAME + ".%l"] + if self._uses_grader(): + return [self.GRADER_BASENAME + ".%l"] else: return [] @@ -162,8 +162,8 @@ def get_auto_managers(self): """See TaskType.get_auto_managers.""" return [self.MANAGER_FILENAME] - def _uses_stub(self) -> bool: - return self.compilation == self.COMPILATION_STUB + def _uses_grader(self) -> bool: + return self.compilation == self.COMPILATION_GRADER def _uses_fifos(self) -> bool: return self.io == self.USER_IO_FIFOS @@ -180,7 +180,7 @@ def _executable_filename(codenames: Iterable[str], language: Language) -> str: """ name = "_".join(sorted(codename.replace(".%l", "") - for codename in codenames)) + for codename in codenames)) return name + language.executable_extension def compile(self, job: CompilationJob, file_cacher: FileCacher): @@ -195,14 +195,14 @@ def compile(self, job: CompilationJob, file_cacher: FileCacher): # compilation command. filenames_to_compile = [] filenames_and_digests_to_get = {} - # The stub, that must have been provided (copy and add to compilation). - if self._uses_stub(): - stub_filename = self.STUB_BASENAME + source_ext - if not check_manager_present(job, stub_filename): + # The grader, that must have been provided (copy and add to compilation). + if self._uses_grader(): + grader_filename = self.GRADER_BASENAME + source_ext + if not check_manager_present(job, grader_filename): return - filenames_to_compile.append(stub_filename) - filenames_and_digests_to_get[stub_filename] = \ - job.managers[stub_filename].digest + filenames_to_compile.append(grader_filename) + filenames_and_digests_to_get[grader_filename] = \ + job.managers[grader_filename].digest # User's submitted file(s) (copy and add to compilation). for codename, file_ in job.files.items(): filename = codename.replace(".%l", source_ext) @@ -335,9 +335,9 @@ def evaluate(self, job: EvaluationJob, file_cacher: FileCacher): # but it's only bool if wait=True, which it isn't here. manager = typing.cast(subprocess.Popen, manager_) - # Start the user submissions compiled with the stub. + # Start the user submissions compiled with the grader. language = get_language(job.language) - main = self.STUB_BASENAME if self._uses_stub() \ + main = self.GRADER_BASENAME if self._uses_grader() \ else os.path.splitext(executable_filename)[0] processes: list[subprocess.Popen] = [None for i in indices] for i in indices: diff --git a/cmscontrib/loaders/italy_yaml.py b/cmscontrib/loaders/italy_yaml.py index 760a34960a..088e8d86f1 100644 --- a/cmscontrib/loaders/italy_yaml.py +++ b/cmscontrib/loaders/italy_yaml.py @@ -600,12 +600,14 @@ def get_task(self, get_statement=True) -> Task | None: if os.path.exists(os.path.join( self.path, "sol", "grader%s" % lang.source_extension)): graders = True - break if os.path.exists(os.path.join( self.path, "sol", "stub%s" % lang.source_extension)): stubs = True - break - if graders: + + if graders and stubs: + logger.fatal("Task contains both sol/grader and sol/stub") + return None + elif graders: # Read grader for each language for lang in LANGUAGES: extension = lang.source_extension @@ -622,21 +624,24 @@ def get_task(self, get_statement=True) -> Task | None: logger.warning("Grader for language %s not found ", lang) compilation_param = "grader" elif stubs: - # Read grader for each language + # Read stub for each language, storing as grader for lang in LANGUAGES: extension = lang.source_extension - grader_filename = os.path.join( + stub_filename = os.path.join( self.path, "sol", "stub%s" % extension) - if os.path.exists(grader_filename): + if os.path.exists(stub_filename): + logger.info( + "Found legacy stub for language %s, importing as grader%s", + lang, extension) digest = self.file_cacher.put_file_from_path( - grader_filename, - "Stub for task %s and language %s" % + stub_filename, + "Grader for task %s and language %s" % (task.name, lang)) args["managers"] += [ - Manager("stub%s" % extension, digest)] + Manager("grader%s" % extension, digest)] else: logger.warning("Stub for language %s not found ", lang) - compilation_param = "stub" + compilation_param = "grader" if graders or stubs: # Read managers with other known file extensions for other_filename in os.listdir(os.path.join(self.path, "sol")): @@ -822,7 +827,7 @@ def get_task(self, get_statement=True) -> Task | None: args["task_type"] = "Communication" args["task_type_parameters"] = \ [num_processes, compilation_param, - io_type or ("fifo_io" if compilation_param == "stub" else "std_io")] + io_type or ("fifo_io" if compilation_param == "grader" else "std_io")] digest = self.file_cacher.put_file_from_path( manager_path, "Manager for task %s" % task.name) diff --git a/cmscontrib/loaders/tps.py b/cmscontrib/loaders/tps.py index c96e5d623d..6886ae76ea 100644 --- a/cmscontrib/loaders/tps.py +++ b/cmscontrib/loaders/tps.py @@ -102,7 +102,7 @@ def _get_task_type_parameters(self, data, task_type, evaluation_param): par_processes = '%s_num_processes' % par_prefix if par_processes not in task_type_parameters: task_type_parameters[par_processes] = 1 - return [task_type_parameters[par_processes], "stub", "std_io"] + return [task_type_parameters[par_processes], "grader", "std_io"] if task_type == 'TwoSteps' or task_type == 'OutputOnly': return [evaluation_param] @@ -318,14 +318,22 @@ def get_task(self, get_statement=True): [filename for filename in os.listdir(graders_dir) if filename != 'manager.cpp'] + + if data['task_type'] == 'Communication': + stubs = [f for f in graders_list if os.path.splitext(f)[0] == 'stub'] + graders = [f for f in graders_list if os.path.splitext(f)[0] == 'grader'] + if stubs and graders: + logger.fatal("Task contains both stub and grader in %s", graders_dir) + return None + for grader_name in graders_list: grader_src = os.path.join(graders_dir, grader_name) digest = self.file_cacher.put_file_from_path( grader_src, "Manager for task %s" % name) if data['task_type'] == 'Communication' \ - and os.path.splitext(grader_name)[0] == 'grader': - grader_name = 'stub' + os.path.splitext(grader_name)[1] + and os.path.splitext(grader_name)[0] == 'stub': + grader_name = 'grader' + os.path.splitext(grader_name)[1] args["managers"][grader_name] = Manager(grader_name, digest) # Manager diff --git a/cmscontrib/updaters/update_49.py b/cmscontrib/updaters/update_49.py new file mode 100644 index 0000000000..ac9b403d02 --- /dev/null +++ b/cmscontrib/updaters/update_49.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2026 Luca Versari +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""A class to update a dump created by CMS. + +Used by DumpImporter and DumpUpdater. + +Renames Communication task managers from stub.%l to grader.%l and updates +the compilation parameter from "stub" to "grader". + +""" + + +class Updater: + + def __init__(self, data): + assert data["_version"] == 48 + self.objs = data + + def run(self): + datasets_task_type = {} + communication_tasks = set() + + for k, v in self.objs.items(): + if k.startswith("_"): + continue + if v.get("_class") == "Dataset": + datasets_task_type[k] = v.get("task_type") + if v.get("task_type") == "Communication": + if "task" in v: + communication_tasks.add(v["task"]) + params = v.get("task_type_parameters") + if isinstance(params, list) and len(params) >= 2: + if params[1] == "stub": + params[1] = "grader" + v["task_type_parameters"] = params + + # Collect existing manager filenames per dataset and user test + dataset_existing_managers = set() + user_test_existing_managers = set() + for k, v in self.objs.items(): + if k.startswith("_"): + continue + if v.get("_class") == "Manager": + dataset_existing_managers.add((v.get("dataset"), v.get("filename"))) + elif v.get("_class") == "UserTestManager": + user_test_existing_managers.add((v.get("user_test"), v.get("filename"))) + + # Check for conflicts and perform renames + for k, v in self.objs.items(): + if k.startswith("_"): + continue + if v.get("_class") == "Manager": + dataset_key = v.get("dataset") + if datasets_task_type.get(dataset_key) == "Communication": + fn = v.get("filename", "") + if fn.startswith("stub."): + new_fn = "grader" + fn[4:] + if (dataset_key, new_fn) in dataset_existing_managers: + raise RuntimeError( + "Cannot update dump: dataset %s contains both %s and %s" + % (dataset_key, fn, new_fn) + ) + v["filename"] = new_fn + dataset_existing_managers.add((dataset_key, new_fn)) + elif v.get("_class") == "UserTestManager": + user_test_key = v.get("user_test") + user_test_obj = self.objs.get(user_test_key, {}) + task_key = user_test_obj.get("task") + if task_key in communication_tasks: + fn = v.get("filename", "") + if fn.startswith("stub."): + new_fn = "grader" + fn[4:] + if (user_test_key, new_fn) in user_test_existing_managers: + raise RuntimeError( + "Cannot update dump: user test %s contains both %s and %s" + % (user_test_key, fn, new_fn) + ) + v["filename"] = new_fn + user_test_existing_managers.add((user_test_key, new_fn)) + + return self.objs diff --git a/cmscontrib/updaters/update_from_1.5.sql b/cmscontrib/updaters/update_from_1.5.sql index 0484bf61b3..906cf643df 100644 --- a/cmscontrib/updaters/update_from_1.5.sql +++ b/cmscontrib/updaters/update_from_1.5.sql @@ -105,4 +105,31 @@ ALTER TABLE contests DROP COLUMN analysis_stop; -- https://github.com/cms-dev/cms/pull/1672 ALTER TABLE contests DROP COLUMN per_user_time; +-- Rename Communication task type compilation parameter from 'stub' to 'grader' +UPDATE datasets +SET task_type_parameters = jsonb_set(task_type_parameters, '{1}', '"grader"') +WHERE task_type = 'Communication' + AND jsonb_array_length(task_type_parameters) >= 2 + AND task_type_parameters->>1 = 'stub'; + +-- Rename Manager filenames from 'stub.%' to 'grader.%' for Communication datasets +UPDATE managers +SET filename = 'grader' || substring(filename from 5) +FROM datasets +WHERE managers.dataset_id = datasets.id + AND datasets.task_type = 'Communication' + AND managers.filename LIKE 'stub.%'; + +-- Rename UserTestManager filenames from 'stub.%' to 'grader.%' for Communication tasks +UPDATE user_test_managers +SET filename = 'grader' || substring(filename from 5) +WHERE user_test_managers.user_test_id IN ( + SELECT ut.id + FROM user_tests ut + JOIN tasks t ON ut.task_id = t.id + JOIN datasets d ON d.task_id = t.id + WHERE d.task_type = 'Communication' +) +AND user_test_managers.filename LIKE 'stub.%'; + COMMIT; diff --git a/cmstestsuite/tasks/communication_fifoio_stubbed/__init__.py b/cmstestsuite/tasks/communication_fifoio_stubbed/__init__.py index 31afeec7d2..87dd9736ff 100644 --- a/cmstestsuite/tasks/communication_fifoio_stubbed/__init__.py +++ b/cmstestsuite/tasks/communication_fifoio_stubbed/__init__.py @@ -27,18 +27,18 @@ "memory_limit_{{dataset_id}}": "128", "task_type_{{dataset_id}}": "Communication", "TaskTypeOptions_{{dataset_id}}_Communication_num_processes": "1", - "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "stub", + "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "grader", "TaskTypeOptions_{{dataset_id}}_Communication_user_io": "fifo_io", "score_type_{{dataset_id}}": "Sum", "score_type_parameters_{{dataset_id}}": "50", } managers = [ - "stub.c", - "stub.cpp", - "stub.pas", - "stub.py", - "stub.java", + "grader.c", + "grader.cpp", + "grader.pas", + "grader.py", + "grader.java", "manager", ] diff --git a/cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.c b/cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.c similarity index 100% rename from cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.c rename to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.c diff --git a/cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.cpp b/cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.cpp similarity index 100% rename from cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.cpp rename to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.cpp diff --git a/cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.java b/cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.java similarity index 97% rename from cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.java rename to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.java index ba030be28d..51afecba06 100644 --- a/cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.java +++ b/cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.java @@ -6,7 +6,7 @@ import java.io.InputStreamReader; import java.io.PrintWriter; -public class stub { +public class grader { public static void main(String[] args) throws FileNotFoundException, IOException { // The order these are opened is very important. It must match diff --git a/cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.pas b/cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.pas similarity index 97% rename from cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.pas rename to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.pas index adccf65aad..25b3579579 100644 --- a/cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.pas +++ b/cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.pas @@ -1,4 +1,4 @@ -program stub; +program grader; uses communication; var diff --git a/cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.py b/cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.py similarity index 100% rename from cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.py rename to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.py diff --git a/cmstestsuite/tasks/communication_many_fifoio_stubbed/__init__.py b/cmstestsuite/tasks/communication_many_fifoio_stubbed/__init__.py index ee34be9e76..70348f565b 100644 --- a/cmstestsuite/tasks/communication_many_fifoio_stubbed/__init__.py +++ b/cmstestsuite/tasks/communication_many_fifoio_stubbed/__init__.py @@ -28,18 +28,18 @@ "memory_limit_{{dataset_id}}": "128", "task_type_{{dataset_id}}": "Communication", "TaskTypeOptions_{{dataset_id}}_Communication_num_processes": "2", - "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "stub", + "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "grader", "TaskTypeOptions_{{dataset_id}}_Communication_user_io": "fifo_io", "score_type_{{dataset_id}}": "Sum", "score_type_parameters_{{dataset_id}}": "50", } managers = [ - "stub.c", - "stub.cpp", - "stub.pas", - "stub.py", - "stub.java", + "grader.c", + "grader.cpp", + "grader.pas", + "grader.py", + "grader.java", "manager", ] diff --git a/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.c b/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.c similarity index 100% rename from cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.c rename to cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.c diff --git a/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.cpp b/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.cpp similarity index 100% rename from cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.cpp rename to cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.cpp diff --git a/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.java b/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.java similarity index 97% rename from cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.java rename to cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.java index 0606dea6bd..d9c8e3073c 100644 --- a/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.java +++ b/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.java @@ -6,7 +6,7 @@ import java.io.InputStreamReader; import java.io.PrintWriter; -public class stub { +public class grader { public static void main(String[] args) throws FileNotFoundException, IOException { // The order these are opened is very important. It must match diff --git a/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.pas b/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.pas similarity index 97% rename from cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.pas rename to cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.pas index 82a2d77247..a50eaf7201 100644 --- a/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.pas +++ b/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.pas @@ -1,4 +1,4 @@ -program stub; +program grader; uses sysutils, user1, user2; var diff --git a/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.py b/cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.py similarity index 100% rename from cmstestsuite/tasks/communication_many_fifoio_stubbed/code/stub.py rename to cmstestsuite/tasks/communication_many_fifoio_stubbed/code/grader.py diff --git a/cmstestsuite/tasks/communication_many_stdio_stubbed/__init__.py b/cmstestsuite/tasks/communication_many_stdio_stubbed/__init__.py index 59d534a6ef..bc24304599 100644 --- a/cmstestsuite/tasks/communication_many_stdio_stubbed/__init__.py +++ b/cmstestsuite/tasks/communication_many_stdio_stubbed/__init__.py @@ -28,18 +28,18 @@ "memory_limit_{{dataset_id}}": "128", "task_type_{{dataset_id}}": "Communication", "TaskTypeOptions_{{dataset_id}}_Communication_num_processes": "2", - "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "stub", + "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "grader", "TaskTypeOptions_{{dataset_id}}_Communication_user_io": "std_io", "score_type_{{dataset_id}}": "Sum", "score_type_parameters_{{dataset_id}}": "50", } managers = [ - "stub.c", - "stub.cpp", - "stub.pas", - "stub.py", - "stub.java", + "grader.c", + "grader.cpp", + "grader.pas", + "grader.py", + "grader.java", "manager", ] diff --git a/cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.c b/cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.c similarity index 100% rename from cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.c rename to cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.c diff --git a/cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.cpp b/cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.cpp similarity index 100% rename from cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.cpp rename to cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.cpp diff --git a/cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.java b/cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.java similarity index 96% rename from cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.java rename to cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.java index e731efe844..d700ebca52 100644 --- a/cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.java +++ b/cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.java @@ -2,7 +2,7 @@ import java.io.InputStreamReader; import java.io.IOException; -public class stub { +public class grader { public static void main(String[] args) throws IOException { int procid = Integer.parseInt(args[0]); diff --git a/cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.pas b/cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.pas similarity index 95% rename from cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.pas rename to cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.pas index ebe2f30bb6..19737bbaad 100644 --- a/cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.pas +++ b/cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.pas @@ -1,4 +1,4 @@ -program stub; +program grader; uses sysutils, user1, user2; var diff --git a/cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.py b/cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.py similarity index 100% rename from cmstestsuite/tasks/communication_many_stdio_stubbed/code/stub.py rename to cmstestsuite/tasks/communication_many_stdio_stubbed/code/grader.py diff --git a/cmstestsuite/tasks/communication_stdio_stubbed/__init__.py b/cmstestsuite/tasks/communication_stdio_stubbed/__init__.py index 77156605f6..3c9739c191 100644 --- a/cmstestsuite/tasks/communication_stdio_stubbed/__init__.py +++ b/cmstestsuite/tasks/communication_stdio_stubbed/__init__.py @@ -27,18 +27,18 @@ "memory_limit_{{dataset_id}}": "128", "task_type_{{dataset_id}}": "Communication", "TaskTypeOptions_{{dataset_id}}_Communication_num_processes": "1", - "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "stub", + "TaskTypeOptions_{{dataset_id}}_Communication_compilation": "grader", "TaskTypeOptions_{{dataset_id}}_Communication_user_io": "std_io", "score_type_{{dataset_id}}": "Sum", "score_type_parameters_{{dataset_id}}": "50", } managers = [ - "stub.c", - "stub.cpp", - "stub.pas", - "stub.py", - "stub.java", + "grader.c", + "grader.cpp", + "grader.pas", + "grader.py", + "grader.java", "manager", ] diff --git a/cmstestsuite/tasks/communication_stdio_stubbed/code/stub.c b/cmstestsuite/tasks/communication_stdio_stubbed/code/grader.c similarity index 100% rename from cmstestsuite/tasks/communication_stdio_stubbed/code/stub.c rename to cmstestsuite/tasks/communication_stdio_stubbed/code/grader.c diff --git a/cmstestsuite/tasks/communication_stdio_stubbed/code/stub.cpp b/cmstestsuite/tasks/communication_stdio_stubbed/code/grader.cpp similarity index 100% rename from cmstestsuite/tasks/communication_stdio_stubbed/code/stub.cpp rename to cmstestsuite/tasks/communication_stdio_stubbed/code/grader.cpp diff --git a/cmstestsuite/tasks/communication_stdio_stubbed/code/stub.java b/cmstestsuite/tasks/communication_stdio_stubbed/code/grader.java similarity index 95% rename from cmstestsuite/tasks/communication_stdio_stubbed/code/stub.java rename to cmstestsuite/tasks/communication_stdio_stubbed/code/grader.java index ad4cb129a6..7aba0bb294 100644 --- a/cmstestsuite/tasks/communication_stdio_stubbed/code/stub.java +++ b/cmstestsuite/tasks/communication_stdio_stubbed/code/grader.java @@ -2,7 +2,7 @@ import java.io.InputStreamReader; import java.io.IOException; -public class stub { +public class grader { public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); diff --git a/cmstestsuite/tasks/communication_stdio_stubbed/code/stub.pas b/cmstestsuite/tasks/communication_stdio_stubbed/code/grader.pas similarity index 93% rename from cmstestsuite/tasks/communication_stdio_stubbed/code/stub.pas rename to cmstestsuite/tasks/communication_stdio_stubbed/code/grader.pas index 35ae5f00a3..1e4130a05d 100644 --- a/cmstestsuite/tasks/communication_stdio_stubbed/code/stub.pas +++ b/cmstestsuite/tasks/communication_stdio_stubbed/code/grader.pas @@ -1,4 +1,4 @@ -program stub; +program grader; uses communication; var diff --git a/cmstestsuite/tasks/communication_stdio_stubbed/code/stub.py b/cmstestsuite/tasks/communication_stdio_stubbed/code/grader.py similarity index 100% rename from cmstestsuite/tasks/communication_stdio_stubbed/code/stub.py rename to cmstestsuite/tasks/communication_stdio_stubbed/code/grader.py diff --git a/cmstestsuite/unit_tests/cmscontrib/Update49Test.py b/cmstestsuite/unit_tests/cmscontrib/Update49Test.py new file mode 100644 index 0000000000..1d8b3c06d9 --- /dev/null +++ b/cmstestsuite/unit_tests/cmscontrib/Update49Test.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2026 Luca Versari +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Tests for updater 49.""" + +import copy +import unittest + +from cmscontrib.updaters.update_49 import Updater + + +class TestUpdate49(unittest.TestCase): + + def test_communication_migration(self): + data = { + "_version": 48, + "1": { + "_class": "Task", + "name": "comm_task", + }, + "2": { + "_class": "Dataset", + "task": "1", + "task_type": "Communication", + "task_type_parameters": [1, "stub", "fifo_io"], + }, + "3": { + "_class": "Manager", + "dataset": "2", + "filename": "stub.cpp", + "digest": "abc", + }, + "4": { + "_class": "Manager", + "dataset": "2", + "filename": "stub.py", + "digest": "def", + }, + "5": { + "_class": "Manager", + "dataset": "2", + "filename": "manager", + "digest": "ghi", + }, + "6": { + "_class": "UserTest", + "task": "1", + }, + "7": { + "_class": "UserTestManager", + "user_test": "6", + "filename": "stub.cpp", + "digest": "abc", + }, + "8": { + "_class": "Task", + "name": "batch_task", + }, + "9": { + "_class": "Dataset", + "task": "8", + "task_type": "Batch", + "task_type_parameters": ["alone", ["input.txt", "output.txt"], "diff"], + }, + "10": { + "_class": "Manager", + "dataset": "9", + "filename": "stub.cpp", + "digest": "xyz", + }, + } + + updater = Updater(copy.deepcopy(data)) + res = updater.run() + + # Communication dataset parameters updated + self.assertEqual(res["2"]["task_type_parameters"], [1, "grader", "fifo_io"]) + # Managers on communication dataset renamed + self.assertEqual(res["3"]["filename"], "grader.cpp") + self.assertEqual(res["4"]["filename"], "grader.py") + self.assertEqual(res["5"]["filename"], "manager") + # UserTestManager on communication task renamed + self.assertEqual(res["7"]["filename"], "grader.cpp") + # Batch dataset and its managers untouched + self.assertEqual(res["9"]["task_type_parameters"], ["alone", ["input.txt", "output.txt"], "diff"]) + self.assertEqual(res["10"]["filename"], "stub.cpp") + + def test_conflict_dataset_raises(self): + data = { + "_version": 48, + "1": { + "_class": "Task", + "name": "comm_task", + }, + "2": { + "_class": "Dataset", + "task": "1", + "task_type": "Communication", + "task_type_parameters": [1, "stub", "fifo_io"], + }, + "3": { + "_class": "Manager", + "dataset": "2", + "filename": "stub.cpp", + "digest": "abc", + }, + "4": { + "_class": "Manager", + "dataset": "2", + "filename": "grader.cpp", + "digest": "xyz", + }, + } + + updater = Updater(copy.deepcopy(data)) + with self.assertRaises(RuntimeError): + updater.run() + + def test_conflict_user_test_raises(self): + data = { + "_version": 48, + "1": { + "_class": "Task", + "name": "comm_task", + }, + "2": { + "_class": "Dataset", + "task": "1", + "task_type": "Communication", + "task_type_parameters": [1, "stub", "fifo_io"], + }, + "6": { + "_class": "UserTest", + "task": "1", + }, + "7": { + "_class": "UserTestManager", + "user_test": "6", + "filename": "stub.cpp", + "digest": "abc", + }, + "8": { + "_class": "UserTestManager", + "user_test": "6", + "filename": "grader.cpp", + "digest": "xyz", + }, + } + + updater = Updater(copy.deepcopy(data)) + with self.assertRaises(RuntimeError): + updater.run() + + +if __name__ == "__main__": + unittest.main() diff --git a/cmstestsuite/unit_tests/grading/tasktypes/CommunicationTest.py b/cmstestsuite/unit_tests/grading/tasktypes/CommunicationTest.py index a810564e8b..8389706ca3 100755 --- a/cmstestsuite/unit_tests/grading/tasktypes/CommunicationTest.py +++ b/cmstestsuite/unit_tests/grading/tasktypes/CommunicationTest.py @@ -37,7 +37,7 @@ FILE_FOO_L1 = File(digest="digest of foo.l1", filename="foo.%l") FILE_BAR_L1 = File(digest="digest of bar.l1", filename="bar.%l") MANAGER = Manager(digest="digest of manager", filename="manager") -STUB_L1 = Manager(digest="digest of stub.l1", filename="stub.l1") +GRADER_L1 = Manager(digest="digest of grader.l1", filename="grader.l1") EXE_FOO = Executable(digest="digest of foo", filename="foo") @@ -50,44 +50,44 @@ def setUp(self): self.languages.update({LANG_1, LANG_2}) def test_single_process(self): - tt = Communication([1, "stub", "fifo_io"]) + tt = Communication([1, "grader", "fifo_io"]) cc = tt.get_compilation_commands(["foo.%l"]) self.assertEqual(cc, { "L1": fake_compilation_commands( - COMPILATION_COMMAND_1, ["stub.l1", "foo.l1"], "foo"), + COMPILATION_COMMAND_1, ["grader.l1", "foo.l1"], "foo"), "L2": fake_compilation_commands( - COMPILATION_COMMAND_2, ["stub.l2", "foo.l2"], "foo.ext"), + COMPILATION_COMMAND_2, ["grader.l2", "foo.l2"], "foo.ext"), }) def test_two_processes(self): # Compilation commands are the same regardless of the number of # processes. - tt = Communication([2, "stub", "fifo_io"]) + tt = Communication([2, "grader", "fifo_io"]) cc = tt.get_compilation_commands(["foo.%l"]) self.assertEqual(cc, { "L1": fake_compilation_commands( - COMPILATION_COMMAND_1, ["stub.l1", "foo.l1"], "foo"), + COMPILATION_COMMAND_1, ["grader.l1", "foo.l1"], "foo"), "L2": fake_compilation_commands( - COMPILATION_COMMAND_2, ["stub.l2", "foo.l2"], "foo.ext"), + COMPILATION_COMMAND_2, ["grader.l2", "foo.l2"], "foo.ext"), }) def test_many_files(self): # Communication supports multiple files in the submission format, that # are just compiled together. - tt = Communication([1, "stub", "fifo_io"]) + tt = Communication([1, "grader", "fifo_io"]) cc = tt.get_compilation_commands(["foo.%l", "bar.%l"]) self.assertEqual(cc, { "L1": fake_compilation_commands( - COMPILATION_COMMAND_1, ["stub.l1", "foo.l1", "bar.l1"], + COMPILATION_COMMAND_1, ["grader.l1", "foo.l1", "bar.l1"], "bar_foo"), "L2": fake_compilation_commands( - COMPILATION_COMMAND_2, ["stub.l2", "foo.l2", "bar.l2"], + COMPILATION_COMMAND_2, ["grader.l2", "foo.l2", "bar.l2"], "bar_foo.ext"), }) - def test_no_stub(self): + def test_no_grader(self): # Submissions can be compiled as stand-alone programs, with no - # stubs. + # graders. tt = Communication([1, "alone", "fifo_io"]) cc = tt.get_compilation_commands(["foo.%l"]) self.assertEqual(cc, { @@ -100,13 +100,13 @@ def test_no_stub(self): def test_std_io(self): # Compilation commands are the same regardless of whether we use # stdin/stdout or pipes. - tt = Communication([1, "stub", "std_io"]) + tt = Communication([1, "grader", "std_io"]) cc = tt.get_compilation_commands(["foo.%l"]) self.assertEqual(cc, { "L1": fake_compilation_commands( - COMPILATION_COMMAND_1, ["stub.l1", "foo.l1"], "foo"), + COMPILATION_COMMAND_1, ["grader.l1", "foo.l1"], "foo"), "L2": fake_compilation_commands( - COMPILATION_COMMAND_2, ["stub.l2", "foo.l2"], "foo.ext"), + COMPILATION_COMMAND_2, ["grader.l2", "foo.l2"], "foo.ext"), }) @@ -151,8 +151,8 @@ def assertResultsInJob( def test_one_file_success(self): tt, job = self.prepare( - [1, "stub", "fifo_io"], - {"foo.%l": FILE_FOO_L1}, {"stub.l1": STUB_L1}) + [1, "grader", "fifo_io"], + {"foo.%l": FILE_FOO_L1}, {"grader.l1": GRADER_L1}) sandbox = self.expect_sandbox() sandbox.get_file_to_storage.return_value = "exe_digest" @@ -160,15 +160,15 @@ def test_one_file_success(self): # Sandbox created with the correct file cacher and name. self.Sandbox.assert_called_once_with(0, self.file_cacher.service.shard, name="compile") - # We need all user source files, and the stub for the same language. + # We need all user source files, and the grader for the same language. sandbox.create_file_from_storage.assert_has_calls( [call("foo.l1", "digest of foo.l1", self.file_cacher), - call("stub.l1", "digest of stub.l1", self.file_cacher)], any_order=True) + call("grader.l1", "digest of grader.l1", self.file_cacher)], any_order=True) self.assertEqual(sandbox.create_file_from_storage.call_count, 2) # Compilation step called correctly. self.compilation_step.assert_called_once_with( sandbox, fake_compilation_commands( - COMPILATION_COMMAND_1, ["stub.l1", "foo.l1"], "foo")) + COMPILATION_COMMAND_1, ["grader.l1", "foo.l1"], "foo")) # Results put in job, executable stored and sandbox deleted. self.assertResultsInJob(job, True, True, TEXT, STATS_OK) sandbox.get_file_to_storage.assert_called_once_with("foo", self.file_cacher, ANY) @@ -176,8 +176,8 @@ def test_one_file_success(self): def test_one_file_compilation_failure(self): tt, job = self.prepare( - [1, "stub", "fifo_io"], - {"foo.%l": FILE_FOO_L1}, {"stub.l1": STUB_L1}) + [1, "grader", "fifo_io"], + {"foo.%l": FILE_FOO_L1}, {"grader.l1": GRADER_L1}) self.compilation_step.return_value = True, False, TEXT, STATS_RE sandbox = self.expect_sandbox() @@ -193,8 +193,8 @@ def test_one_file_compilation_failure(self): def test_one_file_sandbox_failure(self): # Sandbox (or CMS) failure. It's the admins' fault. tt, job = self.prepare( - [1, "stub", "fifo_io"], - {"foo.%l": FILE_FOO_L1}, {"stub.l1": STUB_L1}) + [1, "grader", "fifo_io"], + {"foo.%l": FILE_FOO_L1}, {"grader.l1": GRADER_L1}) self.compilation_step.return_value = False, None, None, None sandbox = self.expect_sandbox() @@ -207,9 +207,9 @@ def test_one_file_sandbox_failure(self): def test_many_files_success(self): tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo.%l": FILE_FOO_L1, "bar.%l": FILE_BAR_L1}, - {"stub.l1": STUB_L1}) + {"grader.l1": GRADER_L1}) sandbox = self.expect_sandbox() sandbox.get_file_to_storage.return_value = "exe_digest" @@ -219,23 +219,23 @@ def test_many_files_success(self): self.Sandbox.assert_called_once_with(0, self.file_cacher.service.shard, name="compile") - # We need all user source files in addition to the stub. + # We need all user source files in addition to the grader. sandbox.create_file_from_storage.assert_has_calls( [call("foo.l1", "digest of foo.l1", self.file_cacher), call("bar.l1", "digest of bar.l1", self.file_cacher), - call("stub.l1", "digest of stub.l1", self.file_cacher)], any_order=True) + call("grader.l1", "digest of grader.l1", self.file_cacher)], any_order=True) self.assertEqual(sandbox.create_file_from_storage.call_count, 3) # Compilation step called correctly. self.compilation_step.assert_called_once_with( sandbox, fake_compilation_commands( - COMPILATION_COMMAND_1, ["stub.l1", "foo.l1", "bar.l1"], + COMPILATION_COMMAND_1, ["grader.l1", "foo.l1", "bar.l1"], "bar_foo")) # Results put in job, executable stored and sandbox deleted. self.assertResultsInJob(job, True, True, TEXT, STATS_OK) sandbox.get_file_to_storage.assert_called_once_with("bar_foo", self.file_cacher, ANY) sandbox.cleanup.assert_called_once_with(delete=True) - def test_no_stub_success(self): + def test_no_grader_success(self): tt, job = self.prepare( [1, "alone", "fifo_io"], {"foo.%l": FILE_FOO_L1}, {}) @@ -250,7 +250,7 @@ def test_no_stub_success(self): name="compile") sandbox.create_file_from_storage.assert_called_once_with( "foo.l1", "digest of foo.l1", self.file_cacher) - # Compilation step called correctly, without the stub. + # Compilation step called correctly, without the grader. self.compilation_step.assert_called_once_with( sandbox, fake_compilation_commands( COMPILATION_COMMAND_1, ["foo.l1"], "foo")) @@ -259,11 +259,11 @@ def test_no_stub_success(self): sandbox.get_file_to_storage.assert_called_once_with("foo", self.file_cacher, ANY) sandbox.cleanup.assert_called_once_with(delete=True) - def test_no_stub_but_stub_given_success(self): - # A stub is given but should be ignored. + def test_no_grader_but_grader_given_success(self): + # A grader is given but should be ignored. tt, job = self.prepare( [1, "alone", "fifo_io"], - {"foo.%l": FILE_FOO_L1}, {"stub.l1": STUB_L1}) + {"foo.%l": FILE_FOO_L1}, {"grader.l1": GRADER_L1}) sandbox = self.expect_sandbox() sandbox.get_file_to_storage.return_value = "exe_digest" @@ -273,13 +273,13 @@ def test_no_stub_but_stub_given_success(self): self.Sandbox.assert_called_once_with(0, self.file_cacher.service.shard, name="compile") - # The stub is put in the sandbox because it is a manager with an + # The grader is put in the sandbox because it is a manager with an # extension that hints that it could be useful for compilations. sandbox.create_file_from_storage.assert_has_calls( [call("foo.l1", "digest of foo.l1", self.file_cacher), - call("stub.l1", "digest of stub.l1", self.file_cacher)], any_order=True) + call("grader.l1", "digest of grader.l1", self.file_cacher)], any_order=True) self.assertEqual(sandbox.create_file_from_storage.call_count, 2) - # Compilation step called correctly, without the stub. + # Compilation step called correctly, without the grader. self.compilation_step.assert_called_once_with( sandbox, fake_compilation_commands( COMPILATION_COMMAND_1, ["foo.l1"], "foo")) @@ -359,7 +359,7 @@ def _set_evaluation_step_return_values( @patch.object(config.sandbox, "trusted_sandbox_max_memory_kib", 1234 * 1024) def test_single_process_success(self): tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr = self.expect_sandbox() @@ -388,7 +388,7 @@ def test_single_process_success(self): # see fake_evaluation_commands in the mixin. cmdline_mgr = ["./manager", "/fifo0/u0_to_m", "/fifo0/m_to_u0"] - cmdline_usr = ["run1", "foo", "stub", + cmdline_usr = ["run1", "foo", "grader", "/fifo0/m_to_u0", "/fifo0/u0_to_m"] self.evaluation_step_before_run.assert_has_calls([ call(sandbox_mgr, cmdline_mgr, 4321, 1234 * 1024 * 1024, @@ -413,7 +413,7 @@ def test_single_process_success_long_time_limit(self): # If the time limit is longer than trusted step default time limit, # the manager run should use the task time limit. tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() self.expect_sandbox() @@ -427,7 +427,7 @@ def test_single_process_success_long_time_limit(self): def test_single_process_missing_manager(self): # Manager is missing, should terminate without creating sandboxes. tt, job = self.prepare( - [1, "stub", "fifo_io"], {"foo": EXE_FOO}, {}) + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {}) tt.evaluate(job, self.file_cacher) @@ -437,7 +437,7 @@ def test_single_process_zero_executables(self): # For some reason, no user executables. Should terminate without # creating sandboxes. tt, job = self.prepare( - [1, "stub", "fifo_io"], {}, {"manager": MANAGER}) + [1, "grader", "fifo_io"], {}, {"manager": MANAGER}) tt.evaluate(job, self.file_cacher) @@ -447,7 +447,7 @@ def test_single_process_many_executables(self): # For some reason, two user executables. Should terminate without # creating sandboxes. tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO, "bar": EXE_FOO}, {"manager": MANAGER}) tt.evaluate(job, self.file_cacher) @@ -457,7 +457,7 @@ def test_single_process_many_executables(self): def test_single_process_manager_failure(self): # Manager had problems, it's not the user's fault. tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr = self.expect_sandbox() @@ -475,7 +475,7 @@ def test_single_process_manager_failure(self): def test_single_process_manager_sandbox_failure(self): # Manager sandbox had problems, it's not the user's fault. tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr = self.expect_sandbox() @@ -494,7 +494,7 @@ def test_single_process_manager_and_user_failure(self): # Manager had problems, it's not the user's fault even if also their # submission had problems. tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr = self.expect_sandbox() @@ -512,7 +512,7 @@ def test_single_process_manager_and_user_failure(self): def test_single_process_user_sandbox_failure(self): # User sandbox had problems, it's not the user's fault. tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr = self.expect_sandbox() @@ -530,7 +530,7 @@ def test_single_process_user_sandbox_failure(self): def test_single_process_user_failure(self): # User program had problems, it's the user's fault. tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr = self.expect_sandbox() @@ -549,7 +549,7 @@ def test_single_process_user_failure(self): def test_single_process_get_output_success(self): tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) job.get_output = True sandbox_mgr = self.expect_sandbox() @@ -569,7 +569,7 @@ def test_single_process_get_output_success(self): def test_single_process_only_execution_success(self): tt, job = self.prepare( - [1, "stub", "fifo_io"], + [1, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) job.only_execution = True self.expect_sandbox() @@ -584,7 +584,7 @@ def test_single_process_only_execution_success(self): def test_single_process_std_io(self): tt, job = self.prepare( - [1, "stub", "std_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) + [1, "grader", "std_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) self.expect_sandbox() sandbox_usr = self.expect_sandbox() @@ -592,7 +592,7 @@ def test_single_process_std_io(self): # Evaluation step called with the right arguments, in particular # redirects and no command line arguments. - cmdline_usr = ["run1", "foo", "stub"] + cmdline_usr = ["run1", "foo", "grader"] self.evaluation_step_before_run.assert_has_calls([ call(sandbox_usr, cmdline_usr, ANY, ANY, dirs_map=ANY, stdin_redirect="/fifo0/m_to_u0", @@ -603,7 +603,7 @@ def test_single_process_std_io(self): @patch.object(config.sandbox, "trusted_sandbox_max_memory_kib", 1234 * 1024) def test_many_processes_success(self): tt, job = self.prepare( - [2, "stub", "fifo_io"], + [2, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr0 = self.expect_sandbox() @@ -637,10 +637,10 @@ def test_many_processes_success(self): cmdline_mgr = ["./manager", "/fifo0/u0_to_m", "/fifo0/m_to_u0", "/fifo1/u1_to_m", "/fifo1/m_to_u1"] - cmdline_usr0 = ["run1", "foo", "stub", - "/fifo0/m_to_u0", "/fifo0/u0_to_m", "0"] - cmdline_usr1 = ["run1", "foo", "stub", - "/fifo1/m_to_u1", "/fifo1/u1_to_m", "1"] + cmdline_usr0 = ["run1", "foo", "grader", + "/fifo0/m_to_u0", "/fifo0/u0_to_m", "0"] + cmdline_usr1 = ["run1", "foo", "grader", + "/fifo1/m_to_u1", "/fifo1/u1_to_m", "1"] self.evaluation_step_before_run.assert_has_calls([ call(sandbox_mgr, cmdline_mgr, 4321, 1234 * 1024 * 1024, dirs_map={ @@ -674,7 +674,7 @@ def test_many_processes_success_long_time_limit(self): # If the time limit is longer than trusted step default time limit, # the manager run should use the task time limit. tt, job = self.prepare( - [2, "stub", "fifo_io"], + [2, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() self.expect_sandbox() @@ -689,7 +689,7 @@ def test_many_processes_success_long_time_limit(self): def test_many_processes_first_user_failure(self): # One of the user programs had problems, it's the user's fault. tt, job = self.prepare( - [2, "stub", "fifo_io"], + [2, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr0 = self.expect_sandbox() @@ -712,7 +712,7 @@ def test_many_processes_first_user_failure(self): def test_many_processes_last_user_failure(self): # One of the user programs had problems, it's the user's fault. tt, job = self.prepare( - [2, "stub", "fifo_io"], + [2, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) sandbox_mgr = self.expect_sandbox() sandbox_usr0 = self.expect_sandbox() @@ -735,7 +735,7 @@ def test_many_processes_last_user_failure(self): def test_many_processes_merged_timeout(self): # Solution was ok, but considering all runtimes, it hit timeout. tt, job = self.prepare( - [2, "stub", "fifo_io"], + [2, "grader", "fifo_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) job.time_limit = 2.5 stats0 = dict(STATS_OK) @@ -766,7 +766,7 @@ def test_many_processes_merged_timeout(self): def test_many_processes_std_io(self): tt, job = self.prepare( - [2, "stub", "std_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) + [2, "grader", "std_io"], {"foo": EXE_FOO}, {"manager": MANAGER}) self.expect_sandbox() sandbox_usr0 = self.expect_sandbox() sandbox_usr1 = self.expect_sandbox() @@ -775,8 +775,8 @@ def test_many_processes_std_io(self): # Evaluation step called with the right arguments, in particular # redirects and only the process index as command line argument. - cmdline_usr0 = ["run1", "foo", "stub", "0"] - cmdline_usr1 = ["run1", "foo", "stub", "1"] + cmdline_usr0 = ["run1", "foo", "grader", "0"] + cmdline_usr1 = ["run1", "foo", "grader", "1"] self.evaluation_step_before_run.assert_has_calls([ call(sandbox_usr0, cmdline_usr0, ANY, ANY, dirs_map=ANY, stdin_redirect="/fifo0/m_to_u0", diff --git a/docs/External contest formats.rst b/docs/External contest formats.rst index 429f51fd2b..3c6263e1f0 100644 --- a/docs/External contest formats.rst +++ b/docs/External contest formats.rst @@ -115,7 +115,7 @@ The following are optional files, that must be present for certain task types or - :file:`gen/GEN`: in the Italian environment, this file describes the parameters for the input generator: each line not composed entirely by white spaces or comments (comments start with ``#`` and end with the end of the line) represents an input file. Here, it is used, in case it contains specially formatted comments, to signal that the score type is :ref:`scoretypes_groupmin`. If a line contains only a comment of the form :samp:`# ST: {score}` then it marks the beginning of a new group assigning at most :samp:`{score}` points, containing all subsequent testcases until the next special comment. If the file does not exists, or does not contain any special comments, the task is given the :ref:`scoretypes_sum` score type. -- :file:`sol/grader.{%l}` (where :samp:`{%l}` here and after means a supported language extension): for tasks of type :ref:`tasktypes_batch`, it is the piece of code that gets compiled together with the submitted solution, and usually takes care of reading the input and writing the output. If one grader is present, the graders for all supported languages must be provided. +- :file:`sol/grader.{%l}` (where :samp:`{%l}` here and after means a supported language extension): for tasks of type :ref:`tasktypes_batch` or :ref:`tasktypes_communication`, it is the piece of code that gets compiled together with the submitted solution, and usually takes care of reading the input and writing the output (or managing communication with :file:`manager`). If one grader is present, the graders for all supported languages must be provided. - :file:`sol/*.h` and :file:`sol/*lib.pas`: if a grader is present, all other files in the :file:`sol` directory that end with ``.h`` or ``lib.pas`` are treated as auxiliary files needed by the compilation of the grader with the submitted solution. @@ -123,8 +123,6 @@ The following are optional files, that must be present for certain task types or - :file:`check/manager`: (also accepted: :file:`cor/manager`) for tasks of type :ref:`tasktypes_communication`, this executable is the program that reads the input and communicates with the user solution. -- :file:`sol/stub.%l`: for tasks of type :ref:`tasktypes_communication`, this is the piece of code that is compiled together with the user submitted code, and is usually used to manage the communication with :file:`manager`. Again, all supported languages must be present. - - :file:`att/*`: each file in this folder is added as an attachment to the task, named as the file's filename. diff --git a/docs/Task types.rst b/docs/Task types.rst index 81a1edc4c9..1931d2e9c3 100644 --- a/docs/Task types.rst +++ b/docs/Task types.rst @@ -79,26 +79,26 @@ Communication tasks are similar to Batch tasks, but should be used when the inpu In practice, Communication tasks have two processes, running in two different sandboxes: - the first (called manager) is entirely controlled by the admins; it reads the input, communicates with the other one, and writes a :ref:`standard manager output`; -- the second is where the contestant's code runs, optionally after being compiled together with an admin-provided stub that helps with the communication with the first process; it doesn't have access to the input, just to what the manager communicates. +- the second is where the contestant's code runs, optionally after being compiled together with an admin-provided grader that helps with the communication with the first process; it doesn't have access to the input, just to what the manager communicates. This setup ensures that the contestant's code cannot access forbidden data, even in the case they have full knowledge of the admin code. The admins must provide an executable manager called ``manager``. It can read the testcase input from stdin, and will also receive as argument the filenames of two FIFOs, from and to the contestant process (in this order). It must write to stdout the outcome and to stderr the message for the contestant (see :ref:`details about the format`). If the contestant's process fails, the output of the manager is ignored, and the outcome will be 0.0 and the message will explain the reason. -Admins can also provide a manager called :file:`stub.{ext}` for each allowed language, where :file:`{ext}` is the standard extension of a source file in that language. The task type can be set up to compile the stub with the contestant's source. Usually, a stub takes care of the communication with the manager, so that the contestants have to implement only a function. As for Batch, admins can also add header file that will be used when compiling the stub and the contestant's source. +Admins can also provide a manager called :file:`grader.{ext}` for each allowed language, where :file:`{ext}` is the standard extension of a source file in that language. The task type can be set up to compile the grader with the contestant's source. Usually, a grader takes care of the communication with the manager, so that the contestants have to implement only a function. As for Batch, admins can also add header files that will be used when compiling the grader and the contestant's source. -The contestant's program, regardless of whether it's compiled with or without a stub, can be set up to communicate with the manager in two ways: through the standard input and output, or through FIFOs (in which case the FIFOs' filenames will be given as arguments, first the one from the manager and then the one to it). +The contestant's program, regardless of whether it's compiled with or without a grader, can be set up to communicate with the manager in two ways: through the standard input and output, or through FIFOs (in which case the FIFOs' filenames will be given as arguments, first the one from the manager and then the one to it). The first parameter of the task type controls the number of user processes. If it is equal to 1, the behavior will be as explained above. If it is an integer N greater than 1, there are a few differences: -- there will be N processes with the contestant's code and the stub (if present) running; +- there will be N processes with the contestant's code and the grader (if present) running; - there will be N pairs of FIFOs, one for each process running the contestant's program; the manager will receive as argument all pairs in order, and each contestant program will receive its own (as arguments or redirected through stdin/stdout); - each copy of the contestant's program will receive as an additional argument its 0-based index within the running programs; - the time limit is checked against the total user time of all the contestant's processes. The submission format must contain one or more filenames ending with ``.%l``. Multiple source files are simply linked together. Usually the number of files to submit is equal to the number of processes. -Communication supports user tests. In addition to the input file, contestant must provide the stub and their source file. The admin-provided manager will be used; the output returned to the contestant will be what the manager writes to the file :file:`output.txt`. +Communication supports user tests. In addition to the input file, contestant must provide the grader and their source file. The admin-provided manager will be used; the output returned to the contestant will be what the manager writes to the file :file:`output.txt`. .. note:: Particular care must be taken for tasks where the communication through the FIFOs is particularly large or frequent. In these cases, the time to send the data may dominate the actual algorithm runtime, thus making it hard to distinguish between different complexities. @@ -161,7 +161,7 @@ TwoSteps has one parameter, similar to Batch's third, that specifies whether to TwoSteps supports user tests; contestants must provide the manager in addition to the input and their sources. -**How to migrate from TwoSteps to Communication.** Any TwoSteps task can be implemented as a Communication task with two processes. The functionalities in the stub should be migrated to Communication's manager, which also must enforce any restriction in the computed data. +**How to migrate from TwoSteps to Communication.** Any TwoSteps task can be implemented as a Communication task with two processes. The functionalities in the grader should be migrated to Communication's manager, which also must enforce any restriction in the computed data. .. _tasktypes_white_diff: