Skip to content
Draft
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
2 changes: 1 addition & 1 deletion cms/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 24 additions & 24 deletions cms/grading/tasktypes/Communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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"
Expand All @@ -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"

Expand All @@ -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",
Expand All @@ -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],
Expand All @@ -153,17 +153,17 @@ 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 []

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
Expand All @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
27 changes: 16 additions & 11 deletions cmscontrib/loaders/italy_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")):
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 11 additions & 3 deletions cmscontrib/loaders/tps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions cmscontrib/updaters/update_49.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3

# Contest Management System - http://cms-dev.github.io/
# Copyright © 2026 Luca Versari <veluca93@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.

"""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
27 changes: 27 additions & 0 deletions cmscontrib/updaters/update_from_1.5.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
12 changes: 6 additions & 6 deletions cmstestsuite/tasks/communication_fifoio_stubbed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading