Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pisek/config/config_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class JudgeType(StrEnum):
cms_communication = "cms-communication"
opendata_v1 = "opendata-v1"
opendata_v2 = "opendata-v2"
codeforces_batch_v1 = "codeforces-batch-v1"


class ShuffleMode(StrEnum):
Expand Down
1 change: 1 addition & 0 deletions pisek/config/task_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ def validate_model(self):
JudgeType.opendata_v1,
JudgeType.opendata_v2,
JudgeType.cms_batch,
JudgeType.codeforces_batch_v1,
],
TaskType.interactive: [JudgeType.cms_communication],
}
Expand Down
11 changes: 11 additions & 0 deletions pisek/task_jobs/checker/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
RunOpendataV1Judge,
RunOpendataV2Judge,
)
from pisek.task_jobs.checker.codeforces_judge import RunCodeforcesBatchV1Judge


def checker_job(
Expand Down Expand Up @@ -86,5 +87,15 @@ def checker_job(
seed,
expected_verdict,
)
elif env.config.tests.judge_type == JudgeType.codeforces_batch_v1:
return RunCodeforcesBatchV1Judge(
env,
env.config.tests.out_judge,
test,
input_,
output,
correct_output,
expected_verdict,
)
else:
raise ValueError(f"Unknown judge type: {env.config.tests.judge_type}")
21 changes: 20 additions & 1 deletion pisek/task_jobs/checker/checker_base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pisek - Tool for developing tasks for programming competitions.
#
# Copyright (c) 2023 Daniel Skýpala <daniel@honza.info>
# Copyright (c) 2023 Daniel Skýpala <skipy@kam.mff.cuni.cz>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -12,7 +12,10 @@

from abc import abstractmethod
from decimal import Decimal
import os
from typing import assert_never, Optional, override
from tempfile import gettempdir
from uuid import uuid4

from pisek.utils.text import tab
from pisek.utils.paths import IInputPath, IOutputPath, LogPath
Expand Down Expand Up @@ -220,3 +223,19 @@ def _checking_message(self) -> str:
return (
f"output {self.output.col(self._env)} for input {self.input.col(self._env)}"
)

@staticmethod
def _invalid_path(name: str) -> str:
return os.path.join(gettempdir(), f"the-{name}-is-not-available-{uuid4()}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: testlib.h will always open all files. While it does not seem to be a problem if a file does not exists, if it does exist, at least its size will be checked. An attacker could create a large file or directory with the right name in /tmp/ to cause the judge to crash.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you would pass all the files always?


def _maybe_input_path(self) -> str:
if self._env.config.judge_needs_in:
return self.input.abspath
else:
return self._invalid_path("input")

def _maybe_correct_output_path(self) -> str:
if self._env.config.judge_needs_out:
return self.correct_output.abspath
else:
return self._invalid_path("correct-output")
21 changes: 3 additions & 18 deletions pisek/task_jobs/checker/cms_judge.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pisek - Tool for developing tasks for programming competitions.
#
# Copyright (c) 2023 Daniel Skýpala <daniel@honza.info>
# Copyright (c) 2023 Daniel Skýpala <skipy@kam.mff.cuni.cz>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -10,11 +10,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import os
from decimal import Decimal, InvalidOperation
from typing import Optional
from tempfile import gettempdir
from uuid import uuid4

from pisek.utils.paths import IInputPath, IOutputPath
from pisek.env.env import Env
Expand Down Expand Up @@ -116,10 +113,6 @@ def __init__(
**kwargs,
)

@staticmethod
def _invalid_path(name: str):
return os.path.join(gettempdir(), f"the-{name}-is-not-available-{uuid4()}")

def _check(self) -> SolutionResult:
config = self._env.config

Expand All @@ -133,16 +126,8 @@ def _check(self) -> SolutionResult:
ProgramRole.judge,
self.judge,
args=[
(
self.input.abspath
if config.judge_needs_in
else RunCMSBatchJudge._invalid_path("input")
),
(
self.correct_output.abspath
if config.judge_needs_out
else RunCMSBatchJudge._invalid_path("output")
),
self._maybe_input_path(),
self._maybe_correct_output_path(),
self.output.abspath,
],
stdout=self.points_file,
Expand Down
100 changes: 100 additions & 0 deletions pisek/task_jobs/checker/codeforces_judge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# pisek - Tool for developing tasks for programming competitions.
#
# Copyright (c) 2026 Daniel Skýpala <skipy@kam.mff.cuni.cz>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from decimal import Decimal

from pisek.utils.paths import IInputPath, IOutputPath
from pisek.env.env import Env
from pisek.config.config_types import ProgramRole
from pisek.config.task_config import RunSection
from pisek.task_jobs.solution.solution_result import (
Verdict,
SolutionResult,
RelativeSolutionResult,
)

from pisek.task_jobs.checker.checker_base import RunBatchChecker


class RunCodeforcesBatchV1Judge(RunBatchChecker):
"""Checks solution output using judge with the codeforces interface. (Abstract class)"""

def __init__(
self,
env: Env,
judge: RunSection,
test: int,
input_: IInputPath,
output: IOutputPath,
correct_output: IOutputPath,
expected_verdict: Verdict | None,
**kwargs,
) -> None:
super().__init__(
env=env,
checker_name=judge.name,
test=test,
input_=input_,
output=output,
correct_output=correct_output,
expected_verdict=expected_verdict,
**kwargs,
)
self.judge = judge

def _check(self) -> SolutionResult:
config = self._env.config
report = self.checker_log_file.replace_suffix(".report")

self._access_file(self.output)
self._access_file(report)
if config.judge_needs_in:
self._access_file(self.input)
if config.judge_needs_out:
self._access_file(self.correct_output)

self.checker_rr = self._run_program(
ProgramRole.judge,
self.judge,
args=[
self._maybe_input_path(),
self.output.abspath,
self._maybe_correct_output_path(),
report.abspath,
],
stderr=self.checker_log_file,
)

if self.checker_rr.returncode == 0:
return RelativeSolutionResult(
verdict=Verdict.ok,
message="OK",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the message be the content of the report file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always treated the message as the thing shown to participants. Also report file can be multiline.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checker comment is shown to participants, just only on samples during the contest proper.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And message is always shown to the participants in the opendata module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a perfect match, but I feel it's kind of like when CMS only shows you the verdict for some test cases.

Regardless of if the comment is the same thing as a message, we should record it and show it in verbose mode. (And, in the future, make it available in the opendata module.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless of if the comment is the same thing as a message, we should record it and show it in verbose mode. (And, in the future, make it available in the opendata module.)

Yes, that is why this is a draft PR.

relative_points=Decimal(1),
)
elif self.checker_rr.returncode == 1:
return RelativeSolutionResult(
verdict=Verdict.wrong_answer,
message="Wrong answer",
relative_points=Decimal(0),
)
elif self.checker_rr.returncode == 2:
return RelativeSolutionResult(
verdict=Verdict.wrong_answer,
message="Presentation error",
relative_points=Decimal(0),
)
else:
raise self._create_program_failure(
f"Judge failed on output {self.output:n}:",
self.checker_rr,
stderr_force_content=True,
)
Loading