Skip to content

Commit

Permalink
Minimal changes for multi-pass problem support.
Browse files Browse the repository at this point in the history
Note that this does not include support for interactive multi-pass
problems.

Currently, we only upload the result of the last pass of each test case
run, which we should obviously fix in the long run but it's easier to do
in separate changes.

General overview of multi-pass problems:

A multi-pass problem requires a validator and that validator is in
charge of deciding whether another pass is being run. In that case, the
file `feedbackdir/nextpass.in` will contain the input of the next pass.
The problem specification includes an upper limit of passes (most often
2), and if the validator generates more passes than the limit, it is
treated as internal error.

Progress towards DOMjudge#2307.

Tested with RagnarGrootKoerkamp/BAPCtools#393:
```
[Sep 28 09:49:05.007] judgedaemon[301583]: Judge started on goo-0 [DOMjudge/8.4.0DEV/3c8c16477]
[Sep 28 09:49:05.007] judgedaemon[301583]: 🔏 Executing chroot script: 'chroot-startstop.sh check'
[Sep 28 09:49:05.015] judgedaemon[301583]: Registering judgehost on endpoint default: http://localhost/domjudge/api
[Sep 28 09:49:05.194] judgedaemon[301583]: No submissions in queue (for endpoint default), waiting...
[Sep 28 09:49:22.049] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:22.050] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/100/118
[Sep 28 09:49:22.051] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:22.194] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:22.578] judgedaemon[301583]:   💻 Compilation: (absolute.py) 'correct'
[Sep 28 09:49:22.646] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:22.647] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:22.882] judgedaemon[301583]:     ✔  ...done in 0.075s (CPU: 0.030s), result: correct
[Sep 28 09:49:22.882] judgedaemon[301583]:     🔄 Running pass 2...
[Sep 28 09:49:23.117] judgedaemon[301583]:     ✔  ...done in 0.044s (CPU: 0.026s), result: correct
[Sep 28 09:49:23.196] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:23.196] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/101/119
[Sep 28 09:49:23.196] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:23.217] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:23.370] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:23.675] judgedaemon[301583]:   💻 Compilation: (deterministic.py) 'correct'
[Sep 28 09:49:23.745] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:23.745] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:23.923] judgedaemon[301583]:     ✔  ...done in 0.042s (CPU: 0.026s), result: correct
[Sep 28 09:49:23.923] judgedaemon[301583]:     🔄 Running pass 2...
[Sep 28 09:49:24.102] judgedaemon[301583]:     ✔  ...done in 0.044s (CPU: 0.025s), result: correct
[Sep 28 09:49:24.174] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:24.175] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/102/120
[Sep 28 09:49:24.175] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:24.193] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:24.336] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:24.708] judgedaemon[301583]:   💻 Compilation: (rand.py) 'correct'
[Sep 28 09:49:24.780] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:24.780] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:25.039] judgedaemon[301583]:     ✔  ...done in 0.129s (CPU: 0.094s), result: correct
[Sep 28 09:49:25.039] judgedaemon[301583]:     🔄 Running pass 2...
[Sep 28 09:49:25.304] judgedaemon[301583]:     ✗  ...done in 0.120s (CPU: 0.091s), result: wrong-answer
[Sep 28 09:49:25.734] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:25.734] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/103/121
[Sep 28 09:49:25.734] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:25.951] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:26.271] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:26.778] judgedaemon[301583]:   💻 Compilation: (no_output.py) 'correct'
[Sep 28 09:49:26.846] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:26.847] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:27.092] judgedaemon[301583]:     ✗  ...done in 0.055s (CPU: 0.025s), result: no-output
[Sep 28 09:49:27.298] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:27.298] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/104/122
[Sep 28 09:49:27.298] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:27.319] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:27.454] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:27.763] judgedaemon[301583]:   💻 Compilation: (string.py) 'correct'
[Sep 28 09:49:27.836] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:27.836] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:28.012] judgedaemon[301583]:     ✗  ...done in 0.068s (CPU: 0.027s), result: wrong-answer
[Sep 28 09:49:28.225] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:28.225] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/105/123
[Sep 28 09:49:28.225] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:28.246] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:28.381] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:28.727] judgedaemon[301583]:   💻 Compilation: (negative.py) 'correct'
[Sep 28 09:49:28.803] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:28.803] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:29.045] judgedaemon[301583]:     ✗  ...done in 0.089s (CPU: 0.056s), result: wrong-answer
[Sep 28 09:49:29.250] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:29.250] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/106/124
[Sep 28 09:49:29.250] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:29.270] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:29.425] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:29.761] judgedaemon[301583]:   💻 Compilation: (negative_second_pass.py) 'correct'
[Sep 28 09:49:29.833] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:29.833] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:30.034] judgedaemon[301583]:     ✔  ...done in 0.080s (CPU: 0.057s), result: correct
[Sep 28 09:49:30.034] judgedaemon[301583]:     🔄 Running pass 2...
[Sep 28 09:49:30.252] judgedaemon[301583]:     ✗  ...done in 0.082s (CPU: 0.060s), result: wrong-answer
[Sep 28 09:49:30.446] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:30.446] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/107/125
[Sep 28 09:49:30.446] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:30.467] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:30.612] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:30.923] judgedaemon[301583]:   💻 Compilation: (extra_output.py) 'correct'
[Sep 28 09:49:30.992] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:30.992] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:31.170] judgedaemon[301583]:     ✔  ...done in 0.055s (CPU: 0.025s), result: correct
[Sep 28 09:49:31.170] judgedaemon[301583]:     🔄 Running pass 2...
[Sep 28 09:49:31.366] judgedaemon[301583]:     ✗  ...done in 0.060s (CPU: 0.028s), result: wrong-answer
[Sep 28 09:49:31.569] judgedaemon[301583]: ⇝ Received 1 'judging_run' judge tasks (endpoint default)
[Sep 28 09:49:31.570] judgedaemon[301583]:   Working directory: /home/sitowert/domjudge/output/judgings/goo-0/endpoint-default/108/126
[Sep 28 09:49:31.570] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:31.590] judgedaemon[301583]:   🔒 Executing chroot script: 'chroot-startstop.sh start'
[Sep 28 09:49:31.725] judgedaemon[301583]:   📋 Verifying versions.
[Sep 28 09:49:32.062] judgedaemon[301583]:   💻 Compilation: (float.py) 'correct'
[Sep 28 09:49:32.132] judgedaemon[301583]:   🏃 Running testcase 7...
[Sep 28 09:49:32.132] judgedaemon[301583]:     🔄 Running pass 1...
[Sep 28 09:49:32.327] judgedaemon[301583]:     ✗  ...done in 0.043s (CPU: 0.022s), result: wrong-answer
[Sep 28 09:49:32.522] judgedaemon[301583]:   🔓 Executing chroot script: 'chroot-startstop.sh stop'
[Sep 28 09:49:32.541] judgedaemon[301583]: No submissions in queue (for endpoint default), waiting...
```
  • Loading branch information
meisterT committed Sep 29, 2024
1 parent ed9f9cf commit a3f0de5
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 73 deletions.
166 changes: 101 additions & 65 deletions judge/judgedaemon.main.php
Original file line number Diff line number Diff line change
Expand Up @@ -1342,21 +1342,6 @@ function judge(array $judgeTask): bool
return false;
}

// Copy program with all possible additional files to testcase
// dir. Use hardlinks to preserve space with big executables.
$programdir = $testcasedir . '/execdir';
system('mkdir -p ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not create directory '$programdir'");
}

foreach (glob("$workdir/compile/*") as $compile_file) {
system('cp -PRl ' . dj_escapeshellarg($compile_file) . ' ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not copy program to '$programdir'");
}
}

// do the actual test-run
$combined_run_compare = $compare_config['combined_run_compare'];
[$run_runpath, $error] = fetch_executable(
Expand Down Expand Up @@ -1403,60 +1388,109 @@ function judge(array $judgeTask): bool
putenv('SCRIPTMEMLIMIT=' . $compare_config['script_memory_limit']);
putenv('SCRIPTFILELIMIT=' . $compare_config['script_filesize_limit']);

$test_run_cmd = LIBJUDGEDIR . "/testcase_run.sh $cpuset_opt " .
implode(' ', array_map('dj_escapeshellarg', [
$tcfile['input'],
$tcfile['output'],
"$run_config[time_limit]:$hardtimelimit",
$testcasedir,
$run_runpath,
$compare_runpath,
$compare_config['compare_args']
]));
system($test_run_cmd, $retval);
$input = $tcfile['input'];
$output = $tcfile['output'];
$passLimit = $run_config['pass_limit'];
for ($passCnt = 1; $passCnt <= $passLimit; $passCnt++) {
$nextPass = false;
if ($passLimit > 1) {
logmsg(LOG_INFO, " 🔄 Running pass $passCnt...");
}

// What does the exitcode mean?
if (! isset($EXITCODES[$retval])) {
alert('error');
error("Unknown exitcode ($retval) from testcase_run.sh for s$judgeTask[submitid]");
}
$result = $EXITCODES[$retval];
$passdir = $testcasedir . '/' . $passCnt;
mkdir($passdir, 0755, true);

// Try to read metadata from file
$runtime = null;
$metadata = read_metadata($testcasedir . '/program.meta');
// Copy program with all possible additional files to testcase
// dir. Use hardlinks to preserve space with big executables.
$programdir = $passdir . '/execdir';
system('mkdir -p ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not create directory '$programdir'");
}

if (isset($metadata['time-used'])) {
$runtime = @$metadata[$metadata['time-used']];
}
foreach (glob("$workdir/compile/*") as $compile_file) {
system('cp -PRl ' . dj_escapeshellarg($compile_file) . ' ' . dj_escapeshellarg($programdir), $retval);
if ($retval!==0) {
error("Could not copy program to '$programdir'");
}
}

if ($result === 'compare-error') {
if ($combined_run_compare) {
logmsg(LOG_ERR, "comparing failed for combined run/compare script '" . $judgeTask['run_script_id'] . "'");
$description = 'combined run/compare script ' . $judgeTask['run_script_id'] . ' crashed';
disable('run_script', 'run_script_id', $judgeTask['run_script_id'], $description, $judgeTask['judgetaskid']);
} else {
logmsg(LOG_ERR, "comparing failed for compare script '" . $judgeTask['compare_script_id'] . "'");
$description = 'compare script ' . $judgeTask['compare_script_id'] . ' crashed';
disable('compare_script', 'compare_script_id', $judgeTask['compare_script_id'], $description, $judgeTask['judgetaskid']);
$test_run_cmd = LIBJUDGEDIR . "/testcase_run.sh $cpuset_opt " .
implode(' ', array_map('dj_escapeshellarg', [
$input,
$output,
"$run_config[time_limit]:$hardtimelimit",
$passdir,
$run_runpath,
$compare_runpath,
$compare_config['compare_args']
]));
system($test_run_cmd, $retval);

// What does the exitcode mean?
if (!isset($EXITCODES[$retval])) {
alert('error');
error("Unknown exitcode ($retval) from testcase_run.sh for s$judgeTask[submitid]");
}
return false;
}
$result = $EXITCODES[$retval];

$new_judging_run = [
'runresult' => urlencode($result),
'runtime' => urlencode((string)$runtime),
'output_run' => rest_encode_file($testcasedir . '/program.out', $output_storage_limit),
'output_error' => rest_encode_file($testcasedir . '/program.err', $output_storage_limit),
'output_system' => rest_encode_file($testcasedir . '/system.out', $output_storage_limit),
'metadata' => rest_encode_file($testcasedir . '/program.meta', false),
'output_diff' => rest_encode_file($testcasedir . '/feedback/judgemessage.txt', $output_storage_limit),
'hostname' => $myhost,
'testcasedir' => $testcasedir,
];
// Try to read metadata from file
$runtime = null;
$metadata = read_metadata($passdir . '/program.meta');

if (file_exists($testcasedir . '/feedback/teammessage.txt')) {
$new_judging_run['team_message'] = rest_encode_file($testcasedir . '/feedback/teammessage.txt', $output_storage_limit);
if (isset($metadata['time-used'])) {
$runtime = @$metadata[$metadata['time-used']];
}

if ($result === 'compare-error') {
if ($combined_run_compare) {
logmsg(LOG_ERR, "comparing failed for combined run/compare script '" . $judgeTask['run_script_id'] . "'");
$description = 'combined run/compare script ' . $judgeTask['run_script_id'] . ' crashed';
disable('run_script', 'run_script_id', $judgeTask['run_script_id'], $description, $judgeTask['judgetaskid']);
} else {
logmsg(LOG_ERR, "comparing failed for compare script '" . $judgeTask['compare_script_id'] . "'");
$description = 'compare script ' . $judgeTask['compare_script_id'] . ' crashed';
disable('compare_script', 'compare_script_id', $judgeTask['compare_script_id'], $description, $judgeTask['judgetaskid']);
}
return false;
}

$new_judging_run = [
'runresult' => urlencode($result),
'runtime' => urlencode((string)$runtime),
'output_run' => rest_encode_file($passdir . '/program.out', $output_storage_limit),
'output_error' => rest_encode_file($passdir . '/program.err', $output_storage_limit),
'output_system' => rest_encode_file($passdir . '/system.out', $output_storage_limit),
'metadata' => rest_encode_file($passdir . '/program.meta', false),
'output_diff' => rest_encode_file($passdir . '/feedback/judgemessage.txt', $output_storage_limit),
'hostname' => $myhost,
'testcasedir' => $testcasedir,
];

if (file_exists($passdir . '/feedback/teammessage.txt')) {
$new_judging_run['team_message'] = rest_encode_file($passdir . '/feedback/teammessage.txt', $output_storage_limit);
}

if ($passLimit > 1) {
$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32m✔\033[0m" : " \033[1;31m✗\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
}

if ($result !== 'correct') {
break;
}
if (file_exists($passdir . '/feedback/nextpass.in')) {
$input = $passdir . '/feedback/nextpass.in';
$nextPass = true;
} else {
break;
}
}
if ($nextPass) {
$description = 'validator produced more passes than allowed ($passLimit)';
disable('compare_script', 'compare_script_id', $judgeTask['compare_script_id'], $description, $judgeTask['judgetaskid']);
return false;
}

$ret = true;
Expand Down Expand Up @@ -1485,9 +1519,11 @@ function judge(array $judgeTask): bool
$ret = (bool)$needsMoreWork;
}

$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32m✔\033[0m" : " \033[1;31m✗\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
if ($passLimit == 1) {
$walltime = $metadata['wall-time'] ?? '?';
logmsg(LOG_INFO, ' ' . ($result === 'correct' ? " \033[0;32m✔\033[0m" : " \033[1;31m✗\033[0m")
. ' ...done in ' . $walltime . 's (CPU: ' . $runtime . 's), result: ' . $result);
}

// done!
return $ret;
Expand Down
2 changes: 1 addition & 1 deletion judge/run-interactive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ MYDIR="$(dirname $0)"

# Run the program while redirecting its stdin/stdout to 'runjury' via
# 'runpipe'. Note that "$@" expands to separate, quoted arguments.
exec ../dj-bin/runpipe ${DEBUG:+-v} -M "$META" -o "$PROGOUT" "$MYDIR/runjury" "$TESTIN" "$TESTOUT" "$FEEDBACK" = "$@"
exec ../../dj-bin/runpipe ${DEBUG:+-v} -M "$META" -o "$PROGOUT" "$MYDIR/runjury" "$TESTIN" "$TESTOUT" "$FEEDBACK" = "$@"
13 changes: 7 additions & 6 deletions judge/testcase_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ cleanup ()
{
# Remove some copied files to save disk space
if [ "$WORKDIR" ]; then
rm -f "$WORKDIR/../dj-bin/runpipe" 2> /dev/null || true
rm -f "$WORKDIR/../../dj-bin/runpipe" 2> /dev/null || true

# Replace testdata by symlinks to reduce disk usage
if [ -f "$WORKDIR/testdata.in" ]; then
Expand Down Expand Up @@ -157,7 +157,8 @@ fi

cd "$WORKDIR"

PREFIX="/$(basename "$PWD")"
# Get the last two directory entries of $PWD
PREFIX="/$(basename $(realpath "$PWD/.."))/$(basename "$PWD")"

# Make testing/execute dir accessible for RUNUSER:
chmod a+x "$WORKDIR" "$WORKDIR/execdir"
Expand All @@ -174,10 +175,10 @@ logmsg $LOG_INFO "setting up testing (chroot) environment"
cp "$TESTIN" "$WORKDIR/testdata.in"

# shellcheck disable=SC2174
mkdir -p -m 0711 ../bin ../dj-bin ../dev
mkdir -p -m 0711 ../../bin ../../dj-bin ../../dev
# copy a support program for interactive problems:
cp -pL "$RUNPIPE" ../dj-bin/runpipe
chmod a+rx ../dj-bin/runpipe
cp -pL "$RUNPIPE" ../../dj-bin/runpipe
chmod a+rx ../../dj-bin/runpipe

# If we need to create a writable temp directory, do so
if [ "$CREATE_WRITABLE_TEMP_DIR" ]; then
Expand All @@ -203,7 +204,7 @@ exitcode=0
# shellcheck disable=SC2153
runcheck "$RUN_SCRIPT" $RUNARGS \
$GAINROOT "$RUNGUARD" ${DEBUG:+-v -V "DEBUG=$DEBUG"} ${TMPDIR:+ -V "TMPDIR=$TMPDIR"} $CPUSET_OPT \
-r "$PWD/.." \
-r "$PWD/../.." \
--nproc=$PROCLIMIT \
--no-core --streamsize=$FILELIMIT \
--user="$RUNUSER" --group="$RUNGROUP" \
Expand Down
36 changes: 36 additions & 0 deletions webapp/migrations/Version20240921081301.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240921081301 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add multipass problem support';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE problem ADD is_multipass_problem TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Whether this problem is a multi-pass problem.\', ADD multipass_limit INT UNSIGNED DEFAULT NULL COMMENT \'Optional limit on the number of rounds for multi-pass problems; defaults to 2 if not specified.\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE problem DROP is_multipass_problem, DROP multipass_limit');
}

public function isTransactional(): bool
{
return false;
}
}
17 changes: 17 additions & 0 deletions webapp/src/Controller/Jury/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public function indexAction(): Response
'memlimit' => ['title' => 'memory limit', 'sort' => true],
'outputlimit' => ['title' => 'output limit', 'sort' => true],
'num_testcases' => ['title' => '# test cases', 'sort' => true],
'type' => ['title' => 'type', 'sort' => true],
];

$contestCountData = $this->em->createQueryBuilder()
Expand Down Expand Up @@ -206,9 +207,17 @@ public function indexAction(): Response
$problemdata['badges'] = ['value' => $badges];

// merge in the rest of the data
$type = '';
if ($p->getCombinedRunCompare()) {
$type .= ' interactive';
}
if ($p->isMultipassProblem()) {
$type .= ' multi-pass';
}
$problemdata = array_merge($problemdata, [
'num_contests' => ['value' => (int)($contestCounts[$p->getProbid()] ?? 0)],
'num_testcases' => ['value' => (int)$row['testdatacount']],
'type' => ['value' => $type],
]);

// Save this to our list of rows
Expand Down Expand Up @@ -484,6 +493,13 @@ public function viewAction(Request $request, SubmissionService $submissionServic
new SubmissionRestriction(problemId: $problem->getProbid()),
);

$type = '';
if ($problem->getCombinedRunCompare()) {
$type .= ' interactive';
}
if ($problem->isMultipassProblem()) {
$type .= ' multi-pass';
}
$data = [
'problem' => $problem,
'problemAttachmentForm' => $problemAttachmentForm->createView(),
Expand All @@ -493,6 +509,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic
'defaultOutputLimit' => (int)$this->config->get('output_limit'),
'defaultRunExecutable' => (string)$this->config->get('default_run'),
'defaultCompareExecutable' => (string)$this->config->get('default_compare'),
'type' => $type,
'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1,
'showExternalResult' => $this->dj->shadowMode(),
'lockedProblem' => $lockedProblem,
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Controller/Jury/SubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ public function viewAction(
'combinedRunCompare' => $submission->getProblem()->getCombinedRunCompare(),
'requestedOutputCount' => $requestedOutputCount,
'version_warnings' => [],
'isMultiPassProblem' => $submission->getProblem()->isMultipassProblem(),
];

if ($selectedJudging === null) {
Expand Down
40 changes: 40 additions & 0 deletions webapp/src/Entity/Problem.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ class Problem extends BaseApiEntity implements
#[Serializer\Exclude]
private ?string $problemstatement_type = null;

#[ORM\Column(options: [
'comment' => 'Whether this problem is a multi-pass problem.',
'default' => 0,
])]
#[Serializer\Exclude]
private bool $isMultipassProblem = false;

#[ORM\Column(
nullable: true,
options: ['comment' => 'Optional limit on the number of rounds; defaults to 1 for traditional problems, 2 for multi-pass problems if not specified.', 'unsigned' => true]
)]
#[Assert\GreaterThan(0)]
#[Serializer\Exclude]
private ?int $multipassLimit = null;

/**
* @var Collection<int, Submission>
*/
Expand Down Expand Up @@ -273,6 +288,31 @@ public function getCombinedRunCompare(): bool
return $this->combined_run_compare;
}

public function setMultipassProblem(bool $isMultipassProblem): Problem
{
$this->isMultipassProblem = $isMultipassProblem;
return $this;
}

public function isMultipassProblem(): bool
{
return $this->isMultipassProblem;
}

public function setMultipassLimit(?int $multipassLimit): Problem
{
$this->multipassLimit = $multipassLimit;
return $this;
}

public function getMultipassLimit(): int
{
if ($this->isMultipassProblem) {
return $this->multipassLimit ?? 2;
}
return 1;
}

public function setProblemstatementFile(?UploadedFile $problemstatementFile): Problem
{
$this->problemstatementFile = $problemstatementFile;
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,7 @@ public function getRunConfig(ContestProblem $problem, Submission $submission): s
'output_limit' => $outputLimit,
'process_limit' => $this->config->get('process_limit'),
'entry_point' => $submission->getEntryPoint(),
'pass_limit' => $problem->getProblem()->getMultipassLimit(),
'hash' => $runExecutable->getHash(),
]
);
Expand Down
Loading

0 comments on commit a3f0de5

Please sign in to comment.