Skip to content

Commit 1f5ed0d

Browse files
committed
Add a new batch-and-output task type.
1 parent b26a7c0 commit 1f5ed0d

File tree

16 files changed

+375
-34
lines changed

16 files changed

+375
-34
lines changed

cms/grading/steps/trusted.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def checker_step(
195195
input_digest: str,
196196
correct_output_digest: str,
197197
output_filename: str,
198+
extra_args: list[str] | None = None
198199
) -> tuple[bool, float | None, list[str] | None]:
199200
"""Run the explicit checker given by the admins
200201
@@ -209,6 +210,7 @@ def checker_step(
209210
as "correct_output.txt".
210211
output_filename: inner filename of the user output (already in the
211212
sandbox).
213+
extra_args: extra arguments to pass to the checker.
212214
213215
return: success (true if the checker was able to check the solution
214216
successfully), outcome and text (both None if success is False).
@@ -240,7 +242,7 @@ def checker_step(
240242
command = ["./%s" % CHECKER_FILENAME,
241243
CHECKER_INPUT_FILENAME,
242244
CHECKER_CORRECT_OUTPUT_FILENAME,
243-
output_filename]
245+
output_filename] + (extra_args if extra_args is not None else [])
244246
box_success, success, unused_stats = trusted_step(sandbox, [command])
245247
if not box_success or not success:
246248
logger.error("Sandbox failed during checker step. "

cms/grading/tasktypes/Batch.py

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class Batch(TaskType):
6969
input, correct output and user output) and should write the
7070
outcome to stdout and the text to stderr.
7171
72+
Note that this class is used as a base class for the BatchAndOutput task
73+
type.
7274
"""
7375
# Codename of the checker, if it is used.
7476
CHECKER_CODENAME = "checker"
@@ -114,7 +116,7 @@ class Batch(TaskType):
114116
ACCEPTED_PARAMETERS = [_COMPILATION, _USE_FILE, _EVALUATION]
115117

116118
@property
117-
def name(self):
119+
def name(self) -> str:
118120
"""See TaskType.name."""
119121
# TODO add some details if a grader/comparator is used, etc...
120122
return "Batch"
@@ -145,7 +147,10 @@ def get_compilation_commands(self, submission_format):
145147
codenames_to_compile = []
146148
if self._uses_grader():
147149
codenames_to_compile.append(self.GRADER_BASENAME + ".%l")
148-
codenames_to_compile.extend(submission_format)
150+
# For regular batch, all parts of the submission format end with %l.
151+
# For batch+output only, some might not.
152+
codenames_to_compile.extend(
153+
[x for x in submission_format if x.endswith('.%l')])
149154
res = dict()
150155
for language in LANGUAGES:
151156
source_ext = language.source_extension
@@ -187,18 +192,14 @@ def _executable_filename(codenames: Iterable[str], language: Language) -> str:
187192
return: a deterministic executable name.
188193
189194
"""
190-
name = "_".join(sorted(codename.replace(".%l", "")
191-
for codename in codenames))
195+
name = "_".join(sorted(codename.replace(".%l", "")
196+
for codename in codenames))
192197
return name + language.executable_extension
193198

194-
def compile(self, job, file_cacher):
195-
"""See TaskType.compile."""
199+
def _do_compile(self, job, file_cacher):
196200
language = get_language(job.language)
197201
source_ext = language.source_extension
198202

199-
if not check_files_number(job, 1, or_more=True):
200-
return
201-
202203
# Create the list of filenames to be passed to the compiler. If we use
203204
# a grader, it needs to be in first position in the command line, and
204205
# we check that it exists.
@@ -215,6 +216,8 @@ def compile(self, job, file_cacher):
215216
job.managers[grader_filename].digest
216217
# User's submitted file(s) (copy and add to compilation).
217218
for codename, file_ in job.files.items():
219+
if not codename.endswith(".%l"):
220+
continue
218221
filename = codename.replace(".%l", source_ext)
219222
filenames_to_compile.append(filename)
220223
filenames_and_digests_to_get[filename] = file_.digest
@@ -256,16 +259,19 @@ def compile(self, job, file_cacher):
256259
# Cleanup.
257260
delete_sandbox(sandbox, job.success, job.keep_sandbox)
258261

259-
def evaluate(self, job, file_cacher):
260-
"""See TaskType.evaluate."""
261-
if not check_executables_number(job, 1):
262+
def compile(self, job, file_cacher):
263+
"""See TaskType.compile."""
264+
if not check_files_number(job, 1, or_more=True):
262265
return
263266

267+
self._do_compile(job, file_cacher)
268+
269+
def _execution_step(self, job, file_cacher):
264270
# Prepare the execution
265271
executable_filename = next(iter(job.executables.keys()))
266272
language = get_language(job.language)
267273
main = self.GRADER_BASENAME if self._uses_grader() \
268-
else os.path.splitext(executable_filename)[0]
274+
else os.path.splitext(executable_filename)[0]
269275
commands = language.get_evaluation_commands(
270276
executable_filename, main=main)
271277
executables_to_get = {
@@ -311,6 +317,7 @@ def evaluate(self, job, file_cacher):
311317

312318
outcome = None
313319
text = None
320+
output_file_params = None
314321

315322
# Error in the sandbox: nothing to do!
316323
if not box_success:
@@ -347,20 +354,41 @@ def evaluate(self, job, file_cacher):
347354
outcome = 0.0
348355
text = [N_("Execution completed successfully")]
349356

350-
# Otherwise evaluate the output file.
357+
# Otherwise prepare to evaluate the output file.
351358
else:
352-
box_success, outcome, text = eval_output(
353-
file_cacher, job,
354-
self.CHECKER_CODENAME
355-
if self._uses_checker() else None,
356-
user_output_path=sandbox.relative_path(
359+
output_file_params = {
360+
'user_output_path': sandbox.relative_path(
357361
self._actual_output),
358-
user_output_filename=self.output_filename)
362+
'user_output_filename': self.output_filename}
363+
364+
return outcome, text, output_file_params, stats, box_success, sandbox
365+
366+
def _evaluate_step(self, job, file_cacher, output_file_params, outcome, text, stats, box_success, sandbox, extra_args):
367+
if box_success:
368+
assert (output_file_params is None) == (outcome is not None)
369+
if output_file_params is not None:
370+
box_success, outcome, text = eval_output(
371+
file_cacher, job,
372+
self.CHECKER_CODENAME
373+
if self._uses_checker() else None,
374+
**output_file_params, extra_args=extra_args)
359375

360376
# Fill in the job with the results.
361377
job.success = box_success
362378
job.outcome = str(outcome) if outcome is not None else None
363379
job.text = text
364380
job.plus = stats
365381

366-
delete_sandbox(sandbox, job.success, job.keep_sandbox)
382+
if sandbox is not None:
383+
delete_sandbox(sandbox, job.success, job.keep_sandbox)
384+
385+
def evaluate(self, job, file_cacher):
386+
"""See TaskType.evaluate."""
387+
if not check_executables_number(job, 1):
388+
return
389+
390+
outcome, text, output_file_params, stats, box_success, sandbox = self._execution_step(
391+
job, file_cacher)
392+
393+
self._evaluate_step(job, file_cacher, output_file_params,
394+
outcome, text, stats, box_success, sandbox, extra_args=None)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python3
2+
3+
# Contest Management System - http://cms-dev.github.io/
4+
# Copyright © 2010-2015 Giovanni Mascellani <[email protected]>
5+
# Copyright © 2010-2018 Stefano Maggiolo <[email protected]>
6+
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
7+
# Copyright © 2012-2014 Luca Wehrstedt <[email protected]>
8+
# Copyright © 2017 Myungwoo Chun <[email protected]>
9+
#
10+
# This program is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU Affero General Public License as
12+
# published by the Free Software Foundation, either version 3 of the
13+
# License, or (at your option) any later version.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU Affero General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU Affero General Public License
21+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
23+
import logging
24+
25+
from cms.grading.ParameterTypes import ParameterTypeString
26+
from .Batch import Batch
27+
28+
29+
logger = logging.getLogger(__name__)
30+
31+
32+
# Dummy function to mark translatable string.
33+
def N_(message):
34+
return message
35+
36+
37+
class BatchAndOutput(Batch):
38+
"""Task type class for a task that is a combination of Batch and
39+
OutputOnly.
40+
41+
Parameters needs to be a list of four elements.
42+
43+
The first element is 'grader' or 'alone': in the first
44+
case, the source file is to be compiled with a provided piece of
45+
software ('grader'); in the other by itself.
46+
47+
The second element is a 2-tuple of the input file name and output file
48+
name. The input file may be '' to denote stdin, and similarly the
49+
output filename may be '' to denote stdout.
50+
51+
The third element is 'diff' or 'comparator' and says whether the
52+
output is compared with a simple diff algorithm or using a
53+
comparator.
54+
55+
The fourth element specifies testcases that *must* be provided as
56+
output-only. If a testcase is not in this list, and is not provided
57+
explicitly, then the provided source file will be used to compute
58+
the output.
59+
60+
Note: the first element is used only in the compilation step; the
61+
others only in the evaluation step.
62+
63+
A comparator can read argv[1], argv[2], argv[3], and argv[4] (respectively,
64+
input, correct output, user output and 'outputonly' if the testcase was an
65+
outputonly-only testcase, 'batch' otherwise) and should write the outcome
66+
to stdout and the text to stderr.
67+
68+
The submission format for tasks of this task type should contain both
69+
a single source file and one or more text files named "output_%s.txt",
70+
where "%s" is the test case index.
71+
72+
"""
73+
74+
# Template for the filename of the output files provided by the user; %s
75+
# represent the testcase codename.
76+
USER_OUTPUT_FILENAME_TEMPLATE = "output_%s.txt"
77+
78+
# Other constants to specify the task type behaviour and parameters.
79+
ALLOW_PARTIAL_SUBMISSION = True
80+
81+
_OUTPUT_ONLY_TESTCASES = ParameterTypeString(
82+
"Comma-separated list of output only testcases",
83+
"output_only_testcases",
84+
"")
85+
86+
ACCEPTED_PARAMETERS = Batch.ACCEPTED_PARAMETERS + [_OUTPUT_ONLY_TESTCASES]
87+
88+
@property
89+
def name(self):
90+
"""See TaskType.name."""
91+
# TODO add some details if a grader/comparator is used, etc...
92+
return "BatchAndOutput"
93+
94+
def __init__(self, parameters):
95+
super().__init__(parameters)
96+
97+
# Data in the parameters that is not in Batch.
98+
self.output_only_testcases: set[str] = set(
99+
self.parameters[3].split(','))
100+
101+
@staticmethod
102+
def _get_user_output_filename(job):
103+
return BatchAndOutput.USER_OUTPUT_FILENAME_TEMPLATE % \
104+
job.operation.testcase_codename
105+
106+
def compile(self, job, file_cacher):
107+
"""See TaskType.compile."""
108+
num_source_files = 0
109+
for (codename, _) in job.files.items():
110+
if codename.endswith(".%l"):
111+
num_source_files += 1
112+
113+
if num_source_files == 0:
114+
# This submission did not have any source files, skip compilation
115+
job.success = True
116+
job.compilation_success = True
117+
job.text = [N_("No compilation needed")]
118+
job.plus = {}
119+
return
120+
121+
self._do_compile(job, file_cacher)
122+
123+
def evaluate(self, job, file_cacher):
124+
"""See TaskType.evaluate."""
125+
126+
user_output_filename = self._get_user_output_filename(job)
127+
128+
output_file_params = None
129+
sandbox = None
130+
outcome = None
131+
text = ""
132+
stats = {}
133+
box_success = True
134+
135+
if user_output_filename in job.files:
136+
output_file_params = {
137+
'user_output_digest': job.files[user_output_filename].digest}
138+
elif job.operation.testcase_codename in self.output_only_testcases:
139+
pass
140+
elif job.executables:
141+
outcome, text, output_file_params, stats, box_success, sandbox = self._execution_step(
142+
job, file_cacher)
143+
144+
if output_file_params is None and outcome is None:
145+
job.success = True
146+
job.outcome = "0.0"
147+
job.text = [N_("File not submitted")]
148+
job.plus = {}
149+
return
150+
151+
if job.operation.testcase_codename in self.output_only_testcases:
152+
extra_args = ['outputonly']
153+
else:
154+
extra_args = ['batch']
155+
156+
self._evaluate_step(job, file_cacher, output_file_params,
157+
outcome, text, stats, box_success, sandbox, extra_args)

cms/grading/tasktypes/util.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ def eval_output(
211211
user_output_path: str | None = None,
212212
user_output_digest: str | None = None,
213213
user_output_filename: str = "",
214+
extra_args: list[str] | None = None
214215
) -> tuple[bool, float | None, list[str] | None]:
215216
"""Evaluate ("check") a user output using a white diff or a checker.
216217
@@ -224,6 +225,7 @@ def eval_output(
224225
using the path (exactly one must be non-None).
225226
user_output_filename: the filename the user was expected to write to,
226227
or empty if stdout (used to return an error to the user).
228+
extra_args: additional arguments to pass to the checker
227229
228230
return: tuple of success (true if the checker was
229231
able to check the solution successfully), outcome and text (both None
@@ -266,7 +268,7 @@ def eval_output(
266268
if checker_codename in job.managers else None
267269
success, outcome, text = checker_step(
268270
sandbox, checker_digest, job.input, job.output,
269-
EVAL_USER_OUTPUT_FILENAME)
271+
EVAL_USER_OUTPUT_FILENAME, extra_args)
270272

271273
delete_sandbox(sandbox, success, job.keep_sandbox)
272274
return success, outcome, text

0 commit comments

Comments
 (0)