Skip to content

Commit 8b5dfc2

Browse files
committed
Rename stub.%l to grader.%l in Interactive task type.
1 parent 9905bcc commit 8b5dfc2

11 files changed

Lines changed: 234 additions & 31 deletions

File tree

cms/grading/tasktypes/Interactive.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ class Interactive(TaskType):
3636

3737
CONTROLLER_FILENAME = "controller"
3838
COMPILATION_ALONE = "alone"
39-
COMPILATION_STUB = "stub"
40-
STUB_BASENAME = "stub"
39+
COMPILATION_GRADER = "grader"
40+
GRADER_BASENAME = "grader"
4141

4242
_COMPILATION = ParameterTypeChoice(
4343
"Compilation",
4444
"compilation",
4545
"",
4646
{
4747
COMPILATION_ALONE: "Submissions are self-sufficient",
48-
COMPILATION_STUB: "Submissions are compiled with a stub",
48+
COMPILATION_GRADER: "Submissions are compiled with a grader",
4949
},
5050
)
5151

@@ -102,8 +102,8 @@ def __init__(self, parameters):
102102
def get_compilation_commands(self, submission_format):
103103
"""See TaskType.get_compilation_commands."""
104104
codenames_to_compile = []
105-
if self._uses_stub():
106-
codenames_to_compile.append(self.STUB_BASENAME + ".%l")
105+
if self._uses_grader():
106+
codenames_to_compile.append(self.GRADER_BASENAME + ".%l")
107107
codenames_to_compile.extend([x for x in submission_format if x.endswith(".%l")])
108108
res = dict()
109109
for language in LANGUAGES:
@@ -120,17 +120,17 @@ def get_compilation_commands(self, submission_format):
120120

121121
def get_user_managers(self):
122122
"""See TaskType.get_user_managers."""
123-
if self._uses_stub():
124-
return [self.STUB_BASENAME + ".%l"]
123+
if self._uses_grader():
124+
return [self.GRADER_BASENAME + ".%l"]
125125
else:
126126
return []
127127

128128
def get_auto_managers(self):
129129
"""See TaskType.get_auto_managers."""
130130
return []
131131

132-
def _uses_stub(self) -> bool:
133-
return self.compilation_type == self.COMPILATION_STUB
132+
def _uses_grader(self) -> bool:
133+
return self.compilation_type == self.COMPILATION_GRADER
134134

135135
def compile(self, job, file_cacher):
136136
"""See TaskType.compile."""
@@ -144,8 +144,8 @@ def compile(self, job, file_cacher):
144144
filenames_and_digests_to_get = {}
145145

146146
# Grader (if needed).
147-
if self._uses_stub():
148-
grader_filename = self.STUB_BASENAME + source_ext
147+
if self._uses_grader():
148+
grader_filename = self.GRADER_BASENAME + source_ext
149149
if not check_manager_present(job, grader_filename):
150150
return
151151
filenames_to_compile.append(grader_filename)

cmscontrib/updaters/update_49.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
2121
Used by DumpImporter and DumpUpdater.
2222
23-
Renames Communication task managers from stub.%l to grader.%l and updates
23+
Renames Communication and Interactive task managers from stub.%l to grader.%l and updates
2424
the compilation parameter from "stub" to "grader".
2525
2626
"""
@@ -34,16 +34,16 @@ def __init__(self, data):
3434

3535
def run(self):
3636
datasets_task_type = {}
37-
communication_tasks = set()
37+
tasks_to_update = set()
3838

3939
for k, v in self.objs.items():
4040
if k.startswith("_"):
4141
continue
4242
if v.get("_class") == "Dataset":
4343
datasets_task_type[k] = v.get("task_type")
44-
if v.get("task_type") == "Communication":
44+
if v.get("task_type") in ("Communication", "Interactive"):
4545
if "task" in v:
46-
communication_tasks.add(v["task"])
46+
tasks_to_update.add(v["task"])
4747
params = v.get("task_type_parameters")
4848
if isinstance(params, list) and len(params) >= 2:
4949
if params[1] == "stub":
@@ -67,7 +67,7 @@ def run(self):
6767
continue
6868
if v.get("_class") == "Manager":
6969
dataset_key = v.get("dataset")
70-
if datasets_task_type.get(dataset_key) == "Communication":
70+
if datasets_task_type.get(dataset_key) in ("Communication", "Interactive"):
7171
fn = v.get("filename", "")
7272
if fn.startswith("stub."):
7373
new_fn = "grader" + fn[4:]
@@ -82,7 +82,7 @@ def run(self):
8282
user_test_key = v.get("user_test")
8383
user_test_obj = self.objs.get(user_test_key, {})
8484
task_key = user_test_obj.get("task")
85-
if task_key in communication_tasks:
85+
if task_key in tasks_to_update:
8686
fn = v.get("filename", "")
8787
if fn.startswith("stub."):
8888
new_fn = "grader" + fn[4:]

cmscontrib/updaters/update_from_1.5.sql

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,30 +106,30 @@ ALTER TABLE contests DROP COLUMN analysis_stop;
106106
ALTER TABLE contests DROP COLUMN per_user_time;
107107

108108
-- https://github.com/cms-dev/cms/pull/1711
109-
-- Rename Communication task type compilation parameter from 'stub' to 'grader'
109+
-- Rename Communication and Interactive task type compilation parameter from 'stub' to 'grader'
110110
UPDATE datasets
111111
SET task_type_parameters = jsonb_set(task_type_parameters, '{1}', '"grader"')
112-
WHERE task_type = 'Communication'
112+
WHERE task_type IN ('Communication', 'Interactive')
113113
AND jsonb_array_length(task_type_parameters) >= 2
114114
AND task_type_parameters->>1 = 'stub';
115115

116-
-- Rename Manager filenames from 'stub.%' to 'grader.%' for Communication datasets
116+
-- Rename Manager filenames from 'stub.%' to 'grader.%' for Communication and Interactive datasets
117117
UPDATE managers
118118
SET filename = 'grader' || substring(filename from 5)
119119
FROM datasets
120120
WHERE managers.dataset_id = datasets.id
121-
AND datasets.task_type = 'Communication'
121+
AND datasets.task_type IN ('Communication', 'Interactive')
122122
AND managers.filename LIKE 'stub.%';
123123

124-
-- Rename UserTestManager filenames from 'stub.%' to 'grader.%' for Communication tasks
124+
-- Rename UserTestManager filenames from 'stub.%' to 'grader.%' for Communication and Interactive tasks
125125
UPDATE user_test_managers
126126
SET filename = 'grader' || substring(filename from 5)
127127
WHERE user_test_managers.user_test_id IN (
128128
SELECT ut.id
129129
FROM user_tests ut
130130
JOIN tasks t ON ut.task_id = t.id
131131
JOIN datasets d ON d.task_id = t.id
132-
WHERE d.task_type = 'Communication'
132+
WHERE d.task_type IN ('Communication', 'Interactive')
133133
)
134134
AND user_test_managers.filename LIKE 'stub.%';
135135

cmstestsuite/Tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
lang for lang in ALL_LANGUAGES if lang not in (LANG_HS, LANG_PHP, LANG_RUST)
7676
]
7777

78-
# Languages for which solutions to be linked to a stub for communication tasks
78+
# Languages for which solutions to be linked to a grader for communication tasks
7979
# are present under `code/`.
8080
# TODO: this should be the same as `ALL_LANGUAGES`.
8181
COMMUNICATION_LANGUAGES = [

cmstestsuite/tasks/interactive/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"time_limit_{{dataset_id}}": "1.0",
2727
"memory_limit_{{dataset_id}}": "128",
2828
"task_type_{{dataset_id}}": "Interactive",
29-
"TaskTypeOptions_{{dataset_id}}_Interactive_compilation": "stub",
29+
"TaskTypeOptions_{{dataset_id}}_Interactive_compilation": "grader",
3030
"TaskTypeOptions_{{dataset_id}}_Interactive_process_limit": "200",
3131
"TaskTypeOptions_{{dataset_id}}_Interactive_concurrent": "true",
3232
"TaskTypeOptions_{{dataset_id}}_Interactive_controller_time_limit": "1.0",
@@ -38,8 +38,8 @@
3838

3939
managers = [
4040
"controller",
41-
"stub.cpp",
42-
"stub.py",
41+
"grader.cpp",
42+
"grader.py",
4343
]
4444

4545
test_cases = [
File renamed without changes.
File renamed without changes.

cmstestsuite/unit_tests/cmscontrib/Update49Test.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,80 @@ def test_communication_migration(self):
100100
self.assertEqual(res["9"]["task_type_parameters"], ["alone", ["input.txt", "output.txt"], "diff"])
101101
self.assertEqual(res["10"]["filename"], "stub.cpp")
102102

103+
def test_interactive_migration(self):
104+
data = {
105+
"_version": 48,
106+
"1": {
107+
"_class": "Task",
108+
"name": "inter_task",
109+
},
110+
"2": {
111+
"_class": "Dataset",
112+
"task": "1",
113+
"task_type": "Interactive",
114+
"task_type_parameters": [200, "stub", True, 128.0, 1.0, 5.0],
115+
},
116+
"3": {
117+
"_class": "Manager",
118+
"dataset": "2",
119+
"filename": "stub.cpp",
120+
"digest": "abc",
121+
},
122+
"4": {
123+
"_class": "Manager",
124+
"dataset": "2",
125+
"filename": "stub.py",
126+
"digest": "def",
127+
},
128+
"5": {
129+
"_class": "Manager",
130+
"dataset": "2",
131+
"filename": "controller",
132+
"digest": "ghi",
133+
},
134+
"6": {
135+
"_class": "UserTest",
136+
"task": "1",
137+
},
138+
"7": {
139+
"_class": "UserTestManager",
140+
"user_test": "6",
141+
"filename": "stub.cpp",
142+
"digest": "abc",
143+
},
144+
"8": {
145+
"_class": "Task",
146+
"name": "batch_task",
147+
},
148+
"9": {
149+
"_class": "Dataset",
150+
"task": "8",
151+
"task_type": "Batch",
152+
"task_type_parameters": ["alone", ["input.txt", "output.txt"], "diff"],
153+
},
154+
"10": {
155+
"_class": "Manager",
156+
"dataset": "9",
157+
"filename": "stub.cpp",
158+
"digest": "xyz",
159+
},
160+
}
161+
162+
updater = Updater(copy.deepcopy(data))
163+
res = updater.run()
164+
165+
# Interactive dataset parameters updated
166+
self.assertEqual(res["2"]["task_type_parameters"], [200, "grader", True, 128.0, 1.0, 5.0])
167+
# Managers on interactive dataset renamed
168+
self.assertEqual(res["3"]["filename"], "grader.cpp")
169+
self.assertEqual(res["4"]["filename"], "grader.py")
170+
self.assertEqual(res["5"]["filename"], "controller")
171+
# UserTestManager on interactive task renamed
172+
self.assertEqual(res["7"]["filename"], "grader.cpp")
173+
# Batch dataset and its managers untouched
174+
self.assertEqual(res["9"]["task_type_parameters"], ["alone", ["input.txt", "output.txt"], "diff"])
175+
self.assertEqual(res["10"]["filename"], "stub.cpp")
176+
103177
def test_conflict_dataset_raises(self):
104178
data = {
105179
"_version": 48,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
"""Tests for the Interactive task type."""
20+
21+
import unittest
22+
from unittest.mock import MagicMock
23+
24+
from cms.db import File, Manager
25+
from cms.grading.Job import CompilationJob
26+
from cms.grading.tasktypes.Interactive import Interactive
27+
from cmstestsuite.unit_tests.grading.tasktypes.tasktypetestutils import (
28+
COMPILATION_COMMAND_1,
29+
COMPILATION_COMMAND_2,
30+
LANG_1,
31+
LANG_2,
32+
STATS_OK,
33+
TEXT,
34+
TaskTypeTestMixin,
35+
fake_compilation_commands,
36+
)
37+
38+
39+
FILE_FOO_L1 = File(digest="digest of foo.l1", filename="foo.%l")
40+
GRADER_L1 = Manager(digest="digest of grader.l1", filename="grader.l1")
41+
42+
43+
class TestInteractiveTaskType(TaskTypeTestMixin, unittest.TestCase):
44+
45+
def setUp(self):
46+
super().setUp()
47+
self.setUpMocks("Interactive")
48+
self.languages.update({LANG_1, LANG_2})
49+
self.file_cacher = MagicMock()
50+
51+
def test_get_compilation_commands_with_grader(self):
52+
tt = Interactive([200, "grader", True, 128.0, 1.0, 5.0])
53+
cc = tt.get_compilation_commands(["foo.%l"])
54+
self.assertEqual(
55+
cc,
56+
{
57+
"L1": fake_compilation_commands(
58+
COMPILATION_COMMAND_1, ["grader.l1", "foo.l1"], "foo"
59+
),
60+
"L2": fake_compilation_commands(
61+
COMPILATION_COMMAND_2, ["grader.l2", "foo.l2"], "foo.ext"
62+
),
63+
},
64+
)
65+
66+
def test_get_compilation_commands_alone(self):
67+
tt = Interactive([200, "alone", True, 128.0, 1.0, 5.0])
68+
cc = tt.get_compilation_commands(["foo.%l"])
69+
self.assertEqual(
70+
cc,
71+
{
72+
"L1": fake_compilation_commands(
73+
COMPILATION_COMMAND_1, ["foo.l1"], "foo"
74+
),
75+
"L2": fake_compilation_commands(
76+
COMPILATION_COMMAND_2, ["foo.l2"], "foo.ext"
77+
),
78+
},
79+
)
80+
81+
def test_get_user_managers_with_grader(self):
82+
tt = Interactive([200, "grader", True, 128.0, 1.0, 5.0])
83+
self.assertEqual(tt.get_user_managers(), ["grader.%l"])
84+
85+
def test_get_user_managers_alone(self):
86+
tt = Interactive([200, "alone", True, 128.0, 1.0, 5.0])
87+
self.assertEqual(tt.get_user_managers(), [])
88+
89+
def test_get_auto_managers(self):
90+
tt = Interactive([200, "grader", True, 128.0, 1.0, 5.0])
91+
self.assertEqual(tt.get_auto_managers(), [])
92+
93+
def test_compile_with_grader(self):
94+
tt = Interactive([200, "grader", True, 128.0, 1.0, 5.0])
95+
job = CompilationJob(
96+
language="L1",
97+
files={"foo.%l": FILE_FOO_L1},
98+
managers={"grader.l1": GRADER_L1},
99+
)
100+
sandbox = self.expect_sandbox()
101+
sandbox.get_file_to_storage.return_value = "exe_digest"
102+
self.compilation_step.return_value = (True, True, TEXT, STATS_OK)
103+
104+
tt.compile(job, self.file_cacher)
105+
106+
self.assertTrue(job.success)
107+
self.assertTrue(job.compilation_success)
108+
sandbox.create_file_from_storage.assert_any_call(
109+
"grader.l1", "digest of grader.l1", self.file_cacher
110+
)
111+
sandbox.create_file_from_storage.assert_any_call(
112+
"foo.l1", "digest of foo.l1", self.file_cacher
113+
)
114+
115+
def test_compile_missing_grader(self):
116+
tt = Interactive([200, "grader", True, 128.0, 1.0, 5.0])
117+
job = CompilationJob(
118+
language="L1",
119+
files={"foo.%l": FILE_FOO_L1},
120+
managers={},
121+
)
122+
tt.compile(job, self.file_cacher)
123+
self.assertFalse(job.success)
124+
125+
126+
if __name__ == "__main__":
127+
unittest.main()

0 commit comments

Comments
 (0)