From d2359775ec6d24541a3d00671d758b4083f57638 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:10:58 +0200 Subject: [PATCH 1/2] Warn on non-opaque colors for problems --- webapp/src/Service/CheckConfigService.php | 3 +++ webapp/src/Utils/Utils.php | 13 +++++++++---- webapp/tests/Unit/Utils/UtilsTest.php | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/webapp/src/Service/CheckConfigService.php b/webapp/src/Service/CheckConfigService.php index 3c2544bbe5..459c50fb44 100644 --- a/webapp/src/Service/CheckConfigService.php +++ b/webapp/src/Service/CheckConfigService.php @@ -474,6 +474,9 @@ public function checkContestsValidate(): ConfigCheckItem if (empty($cp->getColor())) { $result = ($result === 'E' ? 'E' : 'W'); $cperrors[$cid] .= " - No color for problem `" . $cp->getShortname() . "` in contest c" . $cid . "\n"; + } elseif (Utils::parseHexColor($cp->getColor())[3] !== 255) { + $result = ($result === 'E' ? 'E' : 'W'); + $cperrors[$cid] .= " - Semi-transparent color for problem `" . $cp->getShortname() . "` in contest c" . $cid . "\n"; } } } diff --git a/webapp/src/Utils/Utils.php b/webapp/src/Utils/Utils.php index 7d0a191a02..eaa3488887 100644 --- a/webapp/src/Utils/Utils.php +++ b/webapp/src/Utils/Utils.php @@ -343,19 +343,24 @@ public static function convertToColor(string $hex): ?string } /** - * Parse a hex color into it's three RGB values. + * Parse a hex color into it's four RGBA values. * - * @return array{int, int, int} + * @return array{int, int, int, int} */ public static function parseHexColor(string $hex): array { // Source: https://stackoverflow.com/a/21966100 - $length = (strlen($hex) - 1) / 3; + $length = (strlen($hex) - 1) / 4; + if (((strlen($hex) - 1) % 3) == 0) { + $length = (strlen($hex) - 1) / 3; + $hex .= str_repeat('F', $length); + } $fact = [17, 1, 0.062272][$length - 1]; return [ (int)round(hexdec(substr($hex, 1, $length)) * $fact), (int)round(hexdec(substr($hex, 1 + $length, $length)) * $fact), - (int)round(hexdec(substr($hex, 1 + 2 * $length, $length)) * $fact) + (int)round(hexdec(substr($hex, 1 + 2 * $length, $length)) * $fact), + (int)round(hexdec(substr($hex, 1 + 3 * $length, $length))) ]; } diff --git a/webapp/tests/Unit/Utils/UtilsTest.php b/webapp/tests/Unit/Utils/UtilsTest.php index 5f35f6b4ea..f751e12eae 100644 --- a/webapp/tests/Unit/Utils/UtilsTest.php +++ b/webapp/tests/Unit/Utils/UtilsTest.php @@ -321,10 +321,10 @@ public function testConvertToHexConvert(): void public function testParseHexColor(): void { - self::assertEquals([255, 255, 255], Utils::parseHexColor('#ffffff')); - self::assertEquals([0, 0, 0], Utils::parseHexColor('#000000')); - self::assertEquals([171, 205, 239], Utils::parseHexColor('#abcdef')); - self::assertEquals([254, 220, 186], Utils::parseHexColor('#FEDCBA')); + self::assertEquals([255, 255, 255, 255], Utils::parseHexColor('#ffffff')); + self::assertEquals([0, 0, 0, 255], Utils::parseHexColor('#000000')); + self::assertEquals([171, 205, 239, 255], Utils::parseHexColor('#abcdef')); + self::assertEquals([254, 220, 186, 255], Utils::parseHexColor('#FEDCBA')); } public function testComponentToHex(): void From b668e9635560d6f5117f061b6c5b85af23023f96 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+vmcj@users.noreply.github.com> Date: Sat, 9 Aug 2025 18:37:00 +0200 Subject: [PATCH 2/2] Allow for different timelimits per problem type Currently interactive problems need more time due to the pipeline handling. Other problemtypes shouldn't have such requirements but if someone has a slower interactor or compare script for some types this can be helpful. So we both do this to not have `interactive` as special case and to provide people with the option due to unforseen setups. --- etc/db-config.yaml | 6 ++++-- judge/judgedaemon.main.php | 7 +------ webapp/composer.json | 3 ++- webapp/src/Entity/Problem.php | 18 +++++++++++----- webapp/src/Service/ConfigurationService.php | 15 +++++++++++++- webapp/src/Service/DOMJudgeService.php | 23 +++++++++++++++------ 6 files changed, 51 insertions(+), 21 deletions(-) diff --git a/etc/db-config.yaml b/etc/db-config.yaml index 665a48a6e8..05c31e5fe1 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -101,8 +101,10 @@ regex: /^[1-9]\d*$/ error_message: A positive number is required. - name: timelimit_overshoot - type: string - default_value: 1s|10% + type: array_keyval + default_value: + default: "1s|10%" + interactive: "2s+20%" public: false description: Time that submissions are kept running beyond timelimit before being killed. Specify as `Xs` for X seconds, `Y%` as percentage, or a combination of both separated by one of `+|&` for the sum, maximum, or minimum of both. regex: /^\d+[s%]([+|&]\d+[s%])?$/ diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index 314a132f73..cda89344ee 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -1359,9 +1359,6 @@ function judge(array $judgeTask): bool return false; } - // TODO: How do we plan to handle these? - $overshoot = djconfig_get_value('timelimit_overshoot'); - // Check whether we have received an exit signal (but not a graceful exit signal). if (function_exists('pcntl_signal_dispatch')) { pcntl_signal_dispatch(); @@ -1417,9 +1414,7 @@ function judge(array $judgeTask): bool } } - $hardtimelimit = $run_config['time_limit'] - + overshoot_time($run_config['time_limit'], $overshoot) - + $run_config['overshoot']; + $hardtimelimit = $run_config['time_limit'] + $run_config['overshoot']; if ($combined_run_compare) { // This accounts for wall time spent in the validator. We may likely // want to make this configurable in the future. The current factor is diff --git a/webapp/composer.json b/webapp/composer.json index 25d00d3f70..061b5d1391 100644 --- a/webapp/composer.json +++ b/webapp/composer.json @@ -131,7 +131,8 @@ "App\\": "src/" }, "files": [ - "resources/functions.php" + "resources/functions.php", + "../lib/lib.misc.php" ] }, "autoload-dev": { diff --git a/webapp/src/Entity/Problem.php b/webapp/src/Entity/Problem.php index 688063fec9..d54dfde085 100644 --- a/webapp/src/Entity/Problem.php +++ b/webapp/src/Entity/Problem.php @@ -112,7 +112,7 @@ class Problem extends BaseApiEntity implements * @var array */ #[Serializer\Exclude] - private array $typesToString = [ + private static array $typesToString = [ self::TYPE_PASS_FAIL => 'pass-fail', self::TYPE_SCORING => 'scoring', self::TYPE_MULTI_PASS => 'multi-pass', @@ -120,6 +120,14 @@ class Problem extends BaseApiEntity implements self::TYPE_SUBMIT_ANSWER => 'submit-answer', ]; + /** + * @return array + */ + public static function getPossibleProblemTypes(): array + { + return self::$typesToString; + } + #[ORM\Column(options: ['comment' => 'Bitmask of problem types, default is pass-fail.'])] #[Serializer\Exclude] private int $types = self::TYPE_PASS_FAIL; @@ -302,7 +310,7 @@ public function getSpecialCompareArgs(): ?string public function setTypesAsString(array $types): Problem { /** @var array $stringToTypes */ - $stringToTypes = array_flip($this->typesToString); + $stringToTypes = array_flip(self::$typesToString); $typeConstants = []; foreach ($types as $type) { if (!isset($stringToTypes[$type])) { @@ -320,10 +328,10 @@ public function getTypesAsString(): string $typeConstants = $this->getTypes(); $typeStrings = []; foreach ($typeConstants as $type) { - if (!isset($this->typesToString[$type])) { + if (!isset(self::$typesToString[$type])) { throw new Exception("Unknown problem type: '$type'"); } - $typeStrings[] = $this->typesToString[$type]; + $typeStrings[] = self::$typesToString[$type]; } return implode(', ', $typeStrings); } @@ -334,7 +342,7 @@ public function getTypesAsString(): string public function getTypes(): array { $ret = []; - foreach (array_keys($this->typesToString) as $type) { + foreach (array_keys(self::$typesToString) as $type) { if ($this->types & $type) { $ret[] = $type; } diff --git a/webapp/src/Service/ConfigurationService.php b/webapp/src/Service/ConfigurationService.php index cfc308382d..3db0f44783 100644 --- a/webapp/src/Service/ConfigurationService.php +++ b/webapp/src/Service/ConfigurationService.php @@ -7,6 +7,7 @@ use App\Entity\Configuration; use App\Entity\Executable; use App\Entity\Judging; +use App\Entity\Problem; use App\Utils\Utils; use BackedEnum; use Doctrine\ORM\EntityManagerInterface; @@ -215,7 +216,13 @@ public function saveChanges( } if (isset($spec->regex)) { - if (preg_match($spec->regex, (string)$val) === 0) { + if ($spec->type === 'array_keyval') { + foreach($val as $val2) { + if (preg_match($spec->regex, (string)$val2) === 0) { + $errors[$specName] = $spec->errorMessage ?? 'This is not a valid value'; + } + } + } elseif (preg_match($spec->regex, (string)$val) === 0) { $errors[$specName] = $spec->errorMessage ?? 'This is not a valid value'; } } @@ -380,6 +387,12 @@ public function addOptions(ConfigurationSpecification $item): ConfigurationSpeci if ($item->name === 'results_remap') { $item->valueOptions = $item->keyOptions; } + break; + case 'timelimit_overshoot': + $problemTypes = array_merge(['default'], Problem::getPossibleProblemTypes()); + foreach ($problemTypes as $type) { + $item->keyOptions[$type] = $type; + } } if ($item->type === 'enum') { diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 8571dfa81f..438fa48ae3 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -1419,26 +1419,37 @@ public function loadTeam(string $teamId, Contest $contest): Team return $team; } - public function getRunConfig(ContestProblem $problem, Submission $submission, int $overshoot = 0): string + public function getRunConfig(ContestProblem $contestProblem, Submission $submission, int $overshoot_extra = 0): string { - $memoryLimit = $problem->getProblem()->getMemlimit(); - $outputLimit = $problem->getProblem()->getOutputlimit(); + $problem = $contestProblem->getProblem(); + $memoryLimit = $problem->getMemlimit(); + $outputLimit = $problem->getOutputlimit(); + $overshootPerType = $this->config->get('timelimit_overshoot'); + $overshoot = overshoot_time($problem->getTimeLimit(), $overshootPerType['default'] ?? "0s"); + foreach ($problem->getTypes() as $problemTypeID) { + $problemType = $problem::getPossibleProblemTypes()[$problemTypeID]; + $problemTypeOvershoot = $overshootPerType[$problemType] ?? false; + if (!$problemTypeOvershoot) { + continue; + } + $overshoot = max($overshoot, overshoot_time($problem->getTimelimit(), $problemTypeOvershoot)); + } if (empty($memoryLimit)) { $memoryLimit = $this->config->get('memory_limit'); } if (empty($outputLimit)) { $outputLimit = $this->config->get('output_limit'); } - $runExecutable = $this->getImmutableRunExecutable($problem); + $runExecutable = $this->getImmutableRunExecutable($contestProblem); return Utils::jsonEncode( [ - 'time_limit' => $problem->getProblem()->getTimelimit() * $submission->getLanguage()->getTimeFactor(), + 'time_limit' => $problem->getTimelimit() * $submission->getLanguage()->getTimeFactor(), 'memory_limit' => $memoryLimit, 'output_limit' => $outputLimit, 'process_limit' => $this->config->get('process_limit'), 'entry_point' => $submission->getEntryPoint(), - 'pass_limit' => $problem->getProblem()->getMultipassLimit(), + 'pass_limit' => $problem->getMultipassLimit(), 'hash' => $runExecutable->getHash(), 'overshoot' => $overshoot, ]