Skip to content

Commit 9905bcc

Browse files
committed
Rename stub.%l to grader.%l.
Fixes #1705.
1 parent 114df9c commit 9905bcc

34 files changed

Lines changed: 451 additions & 144 deletions

File tree

cms/db/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080

8181
# Instantiate or import these objects.
8282

83-
version = 48
83+
version = 49
8484

8585
engine = create_engine(config.database.url, echo=config.database.debug,
8686
pool_timeout=60, pool_recycle=120)

cms/grading/tasktypes/Communication.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ class Communication(TaskType):
5757
5858
The task type will run *manager*, an admin-provided executable, and one or
5959
more instances of the user solution, optionally compiled together with a
60-
language-specific stub.
60+
language-specific grader.
6161
6262
During the evaluation, the manager and each of the user processes
6363
communicate via FIFOs. The manager will read the input, send it (possibly
6464
with some modifications) to the user process(es). The user processes, either
65-
via functions provided by the stub or by themselves, will communicate with
65+
via functions provided by the grader or by themselves, will communicate with
6666
the manager. Finally, the manager will decide outcome and text, and print
6767
them on stdout and stderr.
6868
@@ -82,9 +82,9 @@ class Communication(TaskType):
8282
"""
8383
# Filename of the manager (the stand-alone, admin-provided program).
8484
MANAGER_FILENAME = "manager"
85-
# Basename of the stub, used in the stub filename and as the main class in
86-
# languages that require us to specify it.
87-
STUB_BASENAME = "stub"
85+
# Basename of the grader, used in the grader filename and as the main class
86+
# in languages that require us to specify it.
87+
GRADER_BASENAME = "grader"
8888
# Filename of the input in the manager sandbox. The content will be
8989
# redirected to stdin, and managers should read from there.
9090
INPUT_FILENAME = "input.txt"
@@ -94,7 +94,7 @@ class Communication(TaskType):
9494

9595
# Constants used in the parameter definition.
9696
COMPILATION_ALONE = "alone"
97-
COMPILATION_STUB = "stub"
97+
COMPILATION_GRADER = "grader"
9898
USER_IO_STD = "std_io"
9999
USER_IO_FIFOS = "fifo_io"
100100

@@ -110,7 +110,7 @@ class Communication(TaskType):
110110
"compilation",
111111
"",
112112
{COMPILATION_ALONE: "Submissions are self-sufficient",
113-
COMPILATION_STUB: "Submissions are compiled with a stub"})
113+
COMPILATION_GRADER: "Submissions are compiled with a grader"})
114114

115115
_USER_IO = ParameterTypeChoice(
116116
"User I/O",
@@ -137,14 +137,14 @@ def __init__(self, parameters):
137137
def get_compilation_commands(self, submission_format):
138138
"""See TaskType.get_compilation_commands."""
139139
codenames_to_compile = []
140-
if self._uses_stub():
141-
codenames_to_compile.append(self.STUB_BASENAME + ".%l")
140+
if self._uses_grader():
141+
codenames_to_compile.append(self.GRADER_BASENAME + ".%l")
142142
codenames_to_compile.extend(submission_format)
143143
res = dict()
144144
for language in LANGUAGES:
145145
source_ext = language.source_extension
146146
executable_filename = self._executable_filename(submission_format,
147-
language)
147+
language)
148148
res[language.name] = language.get_compilation_commands(
149149
[codename.replace(".%l", source_ext)
150150
for codename in codenames_to_compile],
@@ -153,17 +153,17 @@ def get_compilation_commands(self, submission_format):
153153

154154
def get_user_managers(self):
155155
"""See TaskType.get_user_managers."""
156-
if self._uses_stub():
157-
return [self.STUB_BASENAME + ".%l"]
156+
if self._uses_grader():
157+
return [self.GRADER_BASENAME + ".%l"]
158158
else:
159159
return []
160160

161161
def get_auto_managers(self):
162162
"""See TaskType.get_auto_managers."""
163163
return [self.MANAGER_FILENAME]
164164

165-
def _uses_stub(self) -> bool:
166-
return self.compilation == self.COMPILATION_STUB
165+
def _uses_grader(self) -> bool:
166+
return self.compilation == self.COMPILATION_GRADER
167167

168168
def _uses_fifos(self) -> bool:
169169
return self.io == self.USER_IO_FIFOS
@@ -180,7 +180,7 @@ def _executable_filename(codenames: Iterable[str], language: Language) -> str:
180180
181181
"""
182182
name = "_".join(sorted(codename.replace(".%l", "")
183-
for codename in codenames))
183+
for codename in codenames))
184184
return name + language.executable_extension
185185

186186
def compile(self, job: CompilationJob, file_cacher: FileCacher):
@@ -195,14 +195,14 @@ def compile(self, job: CompilationJob, file_cacher: FileCacher):
195195
# compilation command.
196196
filenames_to_compile = []
197197
filenames_and_digests_to_get = {}
198-
# The stub, that must have been provided (copy and add to compilation).
199-
if self._uses_stub():
200-
stub_filename = self.STUB_BASENAME + source_ext
201-
if not check_manager_present(job, stub_filename):
198+
# The grader, that must have been provided (copy and add to compilation).
199+
if self._uses_grader():
200+
grader_filename = self.GRADER_BASENAME + source_ext
201+
if not check_manager_present(job, grader_filename):
202202
return
203-
filenames_to_compile.append(stub_filename)
204-
filenames_and_digests_to_get[stub_filename] = \
205-
job.managers[stub_filename].digest
203+
filenames_to_compile.append(grader_filename)
204+
filenames_and_digests_to_get[grader_filename] = \
205+
job.managers[grader_filename].digest
206206
# User's submitted file(s) (copy and add to compilation).
207207
for codename, file_ in job.files.items():
208208
filename = codename.replace(".%l", source_ext)
@@ -335,9 +335,9 @@ def evaluate(self, job: EvaluationJob, file_cacher: FileCacher):
335335
# but it's only bool if wait=True, which it isn't here.
336336
manager = typing.cast(subprocess.Popen, manager_)
337337

338-
# Start the user submissions compiled with the stub.
338+
# Start the user submissions compiled with the grader.
339339
language = get_language(job.language)
340-
main = self.STUB_BASENAME if self._uses_stub() \
340+
main = self.GRADER_BASENAME if self._uses_grader() \
341341
else os.path.splitext(executable_filename)[0]
342342
processes: list[subprocess.Popen] = [None for i in indices]
343343
for i in indices:

cmscontrib/loaders/italy_yaml.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -600,12 +600,14 @@ def get_task(self, get_statement=True) -> Task | None:
600600
if os.path.exists(os.path.join(
601601
self.path, "sol", "grader%s" % lang.source_extension)):
602602
graders = True
603-
break
604603
if os.path.exists(os.path.join(
605604
self.path, "sol", "stub%s" % lang.source_extension)):
606605
stubs = True
607-
break
608-
if graders:
606+
607+
if graders and stubs:
608+
logger.fatal("Task contains both sol/grader and sol/stub")
609+
return None
610+
elif graders:
609611
# Read grader for each language
610612
for lang in LANGUAGES:
611613
extension = lang.source_extension
@@ -622,21 +624,24 @@ def get_task(self, get_statement=True) -> Task | None:
622624
logger.warning("Grader for language %s not found ", lang)
623625
compilation_param = "grader"
624626
elif stubs:
625-
# Read grader for each language
627+
# Read stub for each language, storing as grader
626628
for lang in LANGUAGES:
627629
extension = lang.source_extension
628-
grader_filename = os.path.join(
630+
stub_filename = os.path.join(
629631
self.path, "sol", "stub%s" % extension)
630-
if os.path.exists(grader_filename):
632+
if os.path.exists(stub_filename):
633+
logger.info(
634+
"Found legacy stub for language %s, importing as grader%s",
635+
lang, extension)
631636
digest = self.file_cacher.put_file_from_path(
632-
grader_filename,
633-
"Stub for task %s and language %s" %
637+
stub_filename,
638+
"Grader for task %s and language %s" %
634639
(task.name, lang))
635640
args["managers"] += [
636-
Manager("stub%s" % extension, digest)]
641+
Manager("grader%s" % extension, digest)]
637642
else:
638643
logger.warning("Stub for language %s not found ", lang)
639-
compilation_param = "stub"
644+
compilation_param = "grader"
640645
if graders or stubs:
641646
# Read managers with other known file extensions
642647
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:
822827
args["task_type"] = "Communication"
823828
args["task_type_parameters"] = \
824829
[num_processes, compilation_param,
825-
io_type or ("fifo_io" if compilation_param == "stub" else "std_io")]
830+
io_type or ("fifo_io" if compilation_param == "grader" else "std_io")]
826831
digest = self.file_cacher.put_file_from_path(
827832
manager_path,
828833
"Manager for task %s" % task.name)

cmscontrib/loaders/tps.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def _get_task_type_parameters(self, data, task_type, evaluation_param):
102102
par_processes = '%s_num_processes' % par_prefix
103103
if par_processes not in task_type_parameters:
104104
task_type_parameters[par_processes] = 1
105-
return [task_type_parameters[par_processes], "stub", "std_io"]
105+
return [task_type_parameters[par_processes], "grader", "std_io"]
106106

107107
if task_type == 'TwoSteps' or task_type == 'OutputOnly':
108108
return [evaluation_param]
@@ -318,14 +318,22 @@ def get_task(self, get_statement=True):
318318
[filename
319319
for filename in os.listdir(graders_dir)
320320
if filename != 'manager.cpp']
321+
322+
if data['task_type'] == 'Communication':
323+
stubs = [f for f in graders_list if os.path.splitext(f)[0] == 'stub']
324+
graders = [f for f in graders_list if os.path.splitext(f)[0] == 'grader']
325+
if stubs and graders:
326+
logger.fatal("Task contains both stub and grader in %s", graders_dir)
327+
return None
328+
321329
for grader_name in graders_list:
322330
grader_src = os.path.join(graders_dir, grader_name)
323331
digest = self.file_cacher.put_file_from_path(
324332
grader_src,
325333
"Manager for task %s" % name)
326334
if data['task_type'] == 'Communication' \
327-
and os.path.splitext(grader_name)[0] == 'grader':
328-
grader_name = 'stub' + os.path.splitext(grader_name)[1]
335+
and os.path.splitext(grader_name)[0] == 'stub':
336+
grader_name = 'grader' + os.path.splitext(grader_name)[1]
329337
args["managers"][grader_name] = Manager(grader_name, digest)
330338

331339
# Manager

cmscontrib/updaters/update_49.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
3+
# Contest Management System - http://cms-dev.github.io/
4+
# Copyright © 2026 Luca Versari <veluca93@gmail.com>
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Affero General Public License as
8+
# published by the Free Software Foundation, either version 3 of the
9+
# License, or (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Affero General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Affero General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
19+
"""A class to update a dump created by CMS.
20+
21+
Used by DumpImporter and DumpUpdater.
22+
23+
Renames Communication task managers from stub.%l to grader.%l and updates
24+
the compilation parameter from "stub" to "grader".
25+
26+
"""
27+
28+
29+
class Updater:
30+
31+
def __init__(self, data):
32+
assert data["_version"] == 48
33+
self.objs = data
34+
35+
def run(self):
36+
datasets_task_type = {}
37+
communication_tasks = set()
38+
39+
for k, v in self.objs.items():
40+
if k.startswith("_"):
41+
continue
42+
if v.get("_class") == "Dataset":
43+
datasets_task_type[k] = v.get("task_type")
44+
if v.get("task_type") == "Communication":
45+
if "task" in v:
46+
communication_tasks.add(v["task"])
47+
params = v.get("task_type_parameters")
48+
if isinstance(params, list) and len(params) >= 2:
49+
if params[1] == "stub":
50+
params[1] = "grader"
51+
v["task_type_parameters"] = params
52+
53+
# Collect existing manager filenames per dataset and user test
54+
dataset_existing_managers = set()
55+
user_test_existing_managers = set()
56+
for k, v in self.objs.items():
57+
if k.startswith("_"):
58+
continue
59+
if v.get("_class") == "Manager":
60+
dataset_existing_managers.add((v.get("dataset"), v.get("filename")))
61+
elif v.get("_class") == "UserTestManager":
62+
user_test_existing_managers.add((v.get("user_test"), v.get("filename")))
63+
64+
# Check for conflicts and perform renames
65+
for k, v in self.objs.items():
66+
if k.startswith("_"):
67+
continue
68+
if v.get("_class") == "Manager":
69+
dataset_key = v.get("dataset")
70+
if datasets_task_type.get(dataset_key) == "Communication":
71+
fn = v.get("filename", "")
72+
if fn.startswith("stub."):
73+
new_fn = "grader" + fn[4:]
74+
if (dataset_key, new_fn) in dataset_existing_managers:
75+
raise RuntimeError(
76+
"Cannot update dump: dataset %s contains both %s and %s"
77+
% (dataset_key, fn, new_fn)
78+
)
79+
v["filename"] = new_fn
80+
dataset_existing_managers.add((dataset_key, new_fn))
81+
elif v.get("_class") == "UserTestManager":
82+
user_test_key = v.get("user_test")
83+
user_test_obj = self.objs.get(user_test_key, {})
84+
task_key = user_test_obj.get("task")
85+
if task_key in communication_tasks:
86+
fn = v.get("filename", "")
87+
if fn.startswith("stub."):
88+
new_fn = "grader" + fn[4:]
89+
if (user_test_key, new_fn) in user_test_existing_managers:
90+
raise RuntimeError(
91+
"Cannot update dump: user test %s contains both %s and %s"
92+
% (user_test_key, fn, new_fn)
93+
)
94+
v["filename"] = new_fn
95+
user_test_existing_managers.add((user_test_key, new_fn))
96+
97+
return self.objs

cmscontrib/updaters/update_from_1.5.sql

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,32 @@ ALTER TABLE contests DROP COLUMN analysis_stop;
105105
-- https://github.com/cms-dev/cms/pull/1672
106106
ALTER TABLE contests DROP COLUMN per_user_time;
107107

108+
-- https://github.com/cms-dev/cms/pull/1711
109+
-- Rename Communication task type compilation parameter from 'stub' to 'grader'
110+
UPDATE datasets
111+
SET task_type_parameters = jsonb_set(task_type_parameters, '{1}', '"grader"')
112+
WHERE task_type = 'Communication'
113+
AND jsonb_array_length(task_type_parameters) >= 2
114+
AND task_type_parameters->>1 = 'stub';
115+
116+
-- Rename Manager filenames from 'stub.%' to 'grader.%' for Communication datasets
117+
UPDATE managers
118+
SET filename = 'grader' || substring(filename from 5)
119+
FROM datasets
120+
WHERE managers.dataset_id = datasets.id
121+
AND datasets.task_type = 'Communication'
122+
AND managers.filename LIKE 'stub.%';
123+
124+
-- Rename UserTestManager filenames from 'stub.%' to 'grader.%' for Communication tasks
125+
UPDATE user_test_managers
126+
SET filename = 'grader' || substring(filename from 5)
127+
WHERE user_test_managers.user_test_id IN (
128+
SELECT ut.id
129+
FROM user_tests ut
130+
JOIN tasks t ON ut.task_id = t.id
131+
JOIN datasets d ON d.task_id = t.id
132+
WHERE d.task_type = 'Communication'
133+
)
134+
AND user_test_managers.filename LIKE 'stub.%';
135+
108136
COMMIT;

cmstestsuite/tasks/communication_fifoio_stubbed/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@
2727
"memory_limit_{{dataset_id}}": "128",
2828
"task_type_{{dataset_id}}": "Communication",
2929
"TaskTypeOptions_{{dataset_id}}_Communication_num_processes": "1",
30-
"TaskTypeOptions_{{dataset_id}}_Communication_compilation": "stub",
30+
"TaskTypeOptions_{{dataset_id}}_Communication_compilation": "grader",
3131
"TaskTypeOptions_{{dataset_id}}_Communication_user_io": "fifo_io",
3232
"score_type_{{dataset_id}}": "Sum",
3333
"score_type_parameters_{{dataset_id}}": "50",
3434
}
3535

3636
managers = [
37-
"stub.c",
38-
"stub.cpp",
39-
"stub.pas",
40-
"stub.py",
41-
"stub.java",
37+
"grader.c",
38+
"grader.cpp",
39+
"grader.pas",
40+
"grader.py",
41+
"grader.java",
4242
"manager",
4343
]
4444

cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.c renamed to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.c

File renamed without changes.

cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.cpp renamed to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.cpp

File renamed without changes.

cmstestsuite/tasks/communication_fifoio_stubbed/code/stub.java renamed to cmstestsuite/tasks/communication_fifoio_stubbed/code/grader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import java.io.InputStreamReader;
77
import java.io.PrintWriter;
88

9-
public class stub {
9+
public class grader {
1010

1111
public static void main(String[] args) throws FileNotFoundException, IOException {
1212
// The order these are opened is very important. It must match

0 commit comments

Comments
 (0)