rbx.cp 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbx/__init__.py +0 -0
- rbx/annotations.py +127 -0
- rbx/autoenum.py +333 -0
- rbx/box/__init__.py +0 -0
- rbx/box/builder.py +77 -0
- rbx/box/cd.py +37 -0
- rbx/box/checkers.py +134 -0
- rbx/box/code.py +185 -0
- rbx/box/compile.py +56 -0
- rbx/box/conftest.py +42 -0
- rbx/box/contest/__init__.py +0 -0
- rbx/box/contest/build_contest_statements.py +347 -0
- rbx/box/contest/contest_package.py +76 -0
- rbx/box/contest/contest_utils.py +20 -0
- rbx/box/contest/main.py +179 -0
- rbx/box/contest/schema.py +155 -0
- rbx/box/contest/statements.py +82 -0
- rbx/box/creation.py +72 -0
- rbx/box/download.py +64 -0
- rbx/box/environment.py +345 -0
- rbx/box/extensions.py +26 -0
- rbx/box/generators.py +478 -0
- rbx/box/generators_test.py +63 -0
- rbx/box/main.py +449 -0
- rbx/box/package.py +316 -0
- rbx/box/packaging/boca/extension.py +27 -0
- rbx/box/packaging/boca/packager.py +245 -0
- rbx/box/packaging/contest_main.py +82 -0
- rbx/box/packaging/main.py +68 -0
- rbx/box/packaging/packager.py +117 -0
- rbx/box/packaging/polygon/packager.py +320 -0
- rbx/box/packaging/polygon/test.py +81 -0
- rbx/box/packaging/polygon/xml_schema.py +106 -0
- rbx/box/presets/__init__.py +503 -0
- rbx/box/presets/fetch.py +70 -0
- rbx/box/presets/lock_schema.py +20 -0
- rbx/box/presets/schema.py +59 -0
- rbx/box/schema.py +394 -0
- rbx/box/solutions.py +792 -0
- rbx/box/solutions_test.py +41 -0
- rbx/box/statements/__init__.py +0 -0
- rbx/box/statements/build_statements.py +359 -0
- rbx/box/statements/builders.py +375 -0
- rbx/box/statements/joiners.py +113 -0
- rbx/box/statements/latex.py +47 -0
- rbx/box/statements/latex_jinja.py +214 -0
- rbx/box/statements/schema.py +138 -0
- rbx/box/stresses.py +292 -0
- rbx/box/stressing/__init__.py +0 -0
- rbx/box/stressing/finder_parser.py +359 -0
- rbx/box/stressing/generator_parser.py +258 -0
- rbx/box/testcases.py +54 -0
- rbx/box/ui/__init__.py +0 -0
- rbx/box/ui/captured_log.py +372 -0
- rbx/box/ui/css/app.tcss +48 -0
- rbx/box/ui/main.py +38 -0
- rbx/box/ui/run.py +209 -0
- rbx/box/validators.py +245 -0
- rbx/box/validators_test.py +15 -0
- rbx/checker.py +128 -0
- rbx/clone.py +197 -0
- rbx/config.py +271 -0
- rbx/conftest.py +38 -0
- rbx/console.py +27 -0
- rbx/create.py +37 -0
- rbx/edit.py +24 -0
- rbx/grading/__init__.py +0 -0
- rbx/grading/caching.py +356 -0
- rbx/grading/conftest.py +33 -0
- rbx/grading/judge/__init__.py +0 -0
- rbx/grading/judge/cacher.py +503 -0
- rbx/grading/judge/digester.py +35 -0
- rbx/grading/judge/sandbox.py +748 -0
- rbx/grading/judge/sandboxes/__init__.py +0 -0
- rbx/grading/judge/sandboxes/isolate.py +683 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
- rbx/grading/judge/sandboxes/timeit.py +217 -0
- rbx/grading/judge/storage.py +284 -0
- rbx/grading/judge/test.py +38 -0
- rbx/grading/judge/testiso.py +54 -0
- rbx/grading/steps.py +522 -0
- rbx/grading/steps_with_caching.py +59 -0
- rbx/grading/steps_with_caching_run_test.py +429 -0
- rbx/grading_utils.py +148 -0
- rbx/hydration.py +101 -0
- rbx/main.py +122 -0
- rbx/metadata.py +105 -0
- rbx/providers/__init__.py +43 -0
- rbx/providers/codeforces.py +73 -0
- rbx/providers/provider.py +26 -0
- rbx/resources/checkers/boilerplate.cpp +20 -0
- rbx/resources/default_config.json +48 -0
- rbx/resources/envs/default.rbx.yml +37 -0
- rbx/resources/envs/isolate.rbx.yml +37 -0
- rbx/resources/packagers/boca/checker.sh +43 -0
- rbx/resources/packagers/boca/compare +53 -0
- rbx/resources/packagers/boca/compile/c +172 -0
- rbx/resources/packagers/boca/compile/cc +173 -0
- rbx/resources/packagers/boca/compile/cpp +172 -0
- rbx/resources/packagers/boca/compile/java +194 -0
- rbx/resources/packagers/boca/compile/kt +155 -0
- rbx/resources/packagers/boca/compile/pas +172 -0
- rbx/resources/packagers/boca/compile/py2 +173 -0
- rbx/resources/packagers/boca/compile/py3 +173 -0
- rbx/resources/packagers/boca/run/c +128 -0
- rbx/resources/packagers/boca/run/cc +128 -0
- rbx/resources/packagers/boca/run/cpp +128 -0
- rbx/resources/packagers/boca/run/java +194 -0
- rbx/resources/packagers/boca/run/kt +159 -0
- rbx/resources/packagers/boca/run/py2 +166 -0
- rbx/resources/packagers/boca/run/py3 +166 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
- rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
- rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
- rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
- rbx/resources/presets/default/preset.rbx.yml +12 -0
- rbx/resources/presets/default/problem/.gitignore +6 -0
- rbx/resources/presets/default/problem/gen.cpp +9 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
- rbx/resources/presets/default/problem/random.py +3 -0
- rbx/resources/presets/default/problem/random.txt +2 -0
- rbx/resources/presets/default/problem/sols/main.cpp +9 -0
- rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
- rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
- rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
- rbx/resources/presets/default/problem/statement/projecao.png +0 -0
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
- rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
- rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
- rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
- rbx/resources/presets/default/problem/validator.cpp +16 -0
- rbx/resources/presets/default/problem/wcmp.cpp +34 -0
- rbx/resources/templates/template.cpp +19 -0
- rbx/run.py +45 -0
- rbx/schema.py +64 -0
- rbx/submit.py +61 -0
- rbx/submitors/__init__.py +18 -0
- rbx/submitors/codeforces.py +120 -0
- rbx/submitors/submitor.py +25 -0
- rbx/test.py +347 -0
- rbx/testcase.py +70 -0
- rbx/testcase_rendering.py +79 -0
- rbx/testdata/box1/gen1.cpp +7 -0
- rbx/testdata/box1/gen2.cpp +9 -0
- rbx/testdata/box1/genScript.py +2 -0
- rbx/testdata/box1/hard-tle.sol.cpp +26 -0
- rbx/testdata/box1/ole.cpp +17 -0
- rbx/testdata/box1/problem.rbx.yml +39 -0
- rbx/testdata/box1/re.sol.cpp +23 -0
- rbx/testdata/box1/sol.cpp +22 -0
- rbx/testdata/box1/tests/1.in +1 -0
- rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
- rbx/testdata/box1/tle.sol.cpp +35 -0
- rbx/testdata/box1/validator.cpp +11 -0
- rbx/testdata/box1/wa.sol.cpp +22 -0
- rbx/testdata/caching/executable.py +1 -0
- rbx/testdata/compatible +0 -0
- rbx/testing_utils.py +65 -0
- rbx/utils.py +162 -0
- rbx_cp-0.5.0.dist-info/LICENSE +201 -0
- rbx_cp-0.5.0.dist-info/METADATA +89 -0
- rbx_cp-0.5.0.dist-info/RECORD +164 -0
- rbx_cp-0.5.0.dist-info/WHEEL +4 -0
- rbx_cp-0.5.0.dist-info/entry_points.txt +4 -0
rbx/grading/steps.py
ADDED
@@ -0,0 +1,522 @@
|
|
1
|
+
import pathlib
|
2
|
+
import shlex
|
3
|
+
import subprocess
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
from rich.text import Text
|
9
|
+
|
10
|
+
from rbx import utils
|
11
|
+
from rbx.config import get_bits_stdcpp, get_jngen, get_testlib
|
12
|
+
from rbx.console import console
|
13
|
+
from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
|
14
|
+
from rbx.grading.judge.storage import copyfileobj
|
15
|
+
|
16
|
+
MAX_STDOUT_LEN = 1024 * 1024 * 128 # 128 MB
|
17
|
+
|
18
|
+
|
19
|
+
class Outcome(Enum):
|
20
|
+
ACCEPTED = 'accepted'
|
21
|
+
WRONG_ANSWER = 'wrong-answer'
|
22
|
+
JUDGE_FAILED = 'judge-failed'
|
23
|
+
RUNTIME_ERROR = 'runtime-error'
|
24
|
+
TIME_LIMIT_EXCEEDED = 'time-limit-exceeded'
|
25
|
+
MEMORY_LIMIT_EXCEEDED = 'memory-limit-exceeded'
|
26
|
+
OUTPUT_LIMIT_EXCEEDED = 'output-limit-exceeded'
|
27
|
+
INTERNAL_ERROR = 'internal-error'
|
28
|
+
|
29
|
+
|
30
|
+
class DigestHolder(BaseModel):
|
31
|
+
value: Optional[str] = None
|
32
|
+
|
33
|
+
|
34
|
+
class GradingLogsHolder(BaseModel):
|
35
|
+
run: Optional['RunLog'] = None
|
36
|
+
cached: bool = False
|
37
|
+
|
38
|
+
|
39
|
+
class DigestOrSource(BaseModel):
|
40
|
+
# Source path relative to the FS.
|
41
|
+
src: Optional[pathlib.Path] = None
|
42
|
+
# Digest if we should get file from storage.
|
43
|
+
digest: Optional[DigestHolder] = None
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def create(data: Union[pathlib.Path, DigestHolder, str]) -> 'DigestOrSource':
|
47
|
+
if isinstance(data, str):
|
48
|
+
return DigestOrSource(digest=DigestHolder(value=data))
|
49
|
+
if isinstance(data, DigestHolder):
|
50
|
+
return DigestOrSource(digest=data)
|
51
|
+
return DigestOrSource(src=data)
|
52
|
+
|
53
|
+
def expand(self) -> Dict[str, Any]:
|
54
|
+
res = {}
|
55
|
+
if self.src is not None:
|
56
|
+
res['src'] = self.src
|
57
|
+
if self.digest is not None:
|
58
|
+
res['digest'] = self.digest
|
59
|
+
return res
|
60
|
+
|
61
|
+
|
62
|
+
class DigestOrDest(BaseModel):
|
63
|
+
# Destination path relative to the FS.
|
64
|
+
dest: Optional[pathlib.Path] = None
|
65
|
+
# Digest if we should get file from storage.
|
66
|
+
digest: Optional[DigestHolder] = None
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
def create(data: Union[pathlib.Path, DigestHolder, str]) -> 'DigestOrDest':
|
70
|
+
if isinstance(data, str):
|
71
|
+
return DigestOrDest(digest=DigestHolder(value=data))
|
72
|
+
if isinstance(data, DigestHolder):
|
73
|
+
return DigestOrDest(digest=data)
|
74
|
+
return DigestOrDest(dest=data)
|
75
|
+
|
76
|
+
def expand(self) -> Dict[str, Any]:
|
77
|
+
res = {}
|
78
|
+
if self.dest is not None:
|
79
|
+
res['dest'] = self.dest
|
80
|
+
if self.digest is not None:
|
81
|
+
res['digest'] = self.digest
|
82
|
+
return res
|
83
|
+
|
84
|
+
|
85
|
+
class GradingFileInput(BaseModel):
|
86
|
+
# Destination path relative to the sandboox.
|
87
|
+
dest: pathlib.Path
|
88
|
+
# Source path relative to the FS.
|
89
|
+
src: Optional[pathlib.Path] = None
|
90
|
+
# Digest if we should get file from storage.
|
91
|
+
digest: Optional[DigestHolder] = None
|
92
|
+
# Whether the destination file should be marked as an executable.
|
93
|
+
executable: bool = False
|
94
|
+
|
95
|
+
|
96
|
+
class GradingFileOutput(BaseModel):
|
97
|
+
# Source path relative to the sandbox.
|
98
|
+
src: pathlib.Path
|
99
|
+
# Destination path relative to the FS.
|
100
|
+
dest: Optional[pathlib.Path] = None
|
101
|
+
# Digest if we should put file in storage.
|
102
|
+
digest: Optional[DigestHolder] = None
|
103
|
+
# Whether the destination file should be marked as an executable.
|
104
|
+
executable: bool = False
|
105
|
+
# Whether the file is optional or not.
|
106
|
+
optional: bool = False
|
107
|
+
# Whether to cap its size
|
108
|
+
maxlen: Optional[int] = None
|
109
|
+
# Whether the file is just an intermediate file that should not be tracked.
|
110
|
+
intermediate: bool = False
|
111
|
+
# Whether to track file through its hash (disable for optimization).
|
112
|
+
hash: bool = True
|
113
|
+
|
114
|
+
|
115
|
+
class GradingArtifacts(BaseModel):
|
116
|
+
# Root directory for the produced artifacts.
|
117
|
+
root: pathlib.Path = pathlib.PosixPath('.')
|
118
|
+
# List of input files to copy to the sandbox.
|
119
|
+
inputs: List[GradingFileInput] = []
|
120
|
+
# List of output files to copy from the sandbox.
|
121
|
+
outputs: List[GradingFileOutput] = []
|
122
|
+
# Capture certain logs of the execution.
|
123
|
+
logs: Optional[GradingLogsHolder] = None
|
124
|
+
|
125
|
+
|
126
|
+
class TestcaseIO(BaseModel):
|
127
|
+
index: int
|
128
|
+
input: Optional[pathlib.Path] = None
|
129
|
+
output: Optional[pathlib.Path] = None
|
130
|
+
|
131
|
+
|
132
|
+
class PreprocessLog(BaseModel):
|
133
|
+
cmd: List[str]
|
134
|
+
exitcode: int
|
135
|
+
log: str
|
136
|
+
|
137
|
+
|
138
|
+
class RunLogMetadata(BaseModel):
|
139
|
+
language: Optional[str] = None
|
140
|
+
|
141
|
+
|
142
|
+
class RunLog(BaseModel):
|
143
|
+
exitcode: int = 0
|
144
|
+
exitstatus: str = SandboxBase.EXIT_SANDBOX_ERROR
|
145
|
+
time: Optional[float] = 0.0
|
146
|
+
memory: Optional[int] = 0
|
147
|
+
metadata: Optional[RunLogMetadata] = None
|
148
|
+
|
149
|
+
def get_run_language(self) -> Optional[str]:
|
150
|
+
if self.metadata is None:
|
151
|
+
return None
|
152
|
+
return self.metadata.language
|
153
|
+
|
154
|
+
|
155
|
+
class TestcaseLog(RunLog):
|
156
|
+
stdout_absolute_path: Optional[pathlib.Path] = None
|
157
|
+
stderr_absolute_path: Optional[pathlib.Path] = None
|
158
|
+
log_absolute_path: Optional[pathlib.Path] = None
|
159
|
+
|
160
|
+
|
161
|
+
class CheckerResult(BaseModel):
|
162
|
+
outcome: Outcome
|
163
|
+
message: str = ''
|
164
|
+
no_tle_outcome: Optional[Outcome] = None
|
165
|
+
|
166
|
+
|
167
|
+
class Evaluation(BaseModel):
|
168
|
+
result: CheckerResult
|
169
|
+
testcase: TestcaseIO
|
170
|
+
log: TestcaseLog
|
171
|
+
|
172
|
+
|
173
|
+
def _process_input_artifacts(artifacts: GradingArtifacts, sandbox: SandboxBase):
|
174
|
+
for input_artifact in artifacts.inputs:
|
175
|
+
if input_artifact.digest is not None:
|
176
|
+
assert input_artifact.digest.value is not None
|
177
|
+
sandbox.create_file_from_storage(
|
178
|
+
input_artifact.dest,
|
179
|
+
input_artifact.digest.value,
|
180
|
+
override=True,
|
181
|
+
executable=input_artifact.executable,
|
182
|
+
try_symlink=True,
|
183
|
+
)
|
184
|
+
continue
|
185
|
+
assert input_artifact.src is not None
|
186
|
+
sandbox.create_file_from_other_file(
|
187
|
+
input_artifact.dest,
|
188
|
+
artifacts.root / input_artifact.src,
|
189
|
+
executable=input_artifact.executable,
|
190
|
+
override=True,
|
191
|
+
try_symlink=True,
|
192
|
+
)
|
193
|
+
|
194
|
+
|
195
|
+
def _process_output_artifacts(
|
196
|
+
artifacts: GradingArtifacts, sandbox: SandboxBase
|
197
|
+
) -> bool:
|
198
|
+
for output_artifact in artifacts.outputs:
|
199
|
+
if output_artifact.hash and output_artifact.digest is None:
|
200
|
+
output_artifact.digest = DigestHolder()
|
201
|
+
if not sandbox.file_exists(output_artifact.src):
|
202
|
+
if output_artifact.optional:
|
203
|
+
continue
|
204
|
+
console.print(
|
205
|
+
f'[error]Output artifact [item]{output_artifact.src}[/item] does not exist.[/error]'
|
206
|
+
)
|
207
|
+
return False
|
208
|
+
|
209
|
+
if output_artifact.digest is not None:
|
210
|
+
output_artifact.digest.value = sandbox.get_file_to_storage(
|
211
|
+
output_artifact.src, trunc_len=output_artifact.maxlen
|
212
|
+
)
|
213
|
+
if output_artifact.dest is None:
|
214
|
+
continue
|
215
|
+
dst: pathlib.Path = artifacts.root / output_artifact.dest
|
216
|
+
# Ensure dst directory exists.
|
217
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
218
|
+
with dst.open('wb') as f:
|
219
|
+
with sandbox.get_file(output_artifact.src) as sb_f:
|
220
|
+
copyfileobj(
|
221
|
+
sb_f,
|
222
|
+
f,
|
223
|
+
maxlen=output_artifact.maxlen,
|
224
|
+
)
|
225
|
+
if output_artifact.executable:
|
226
|
+
dst.chmod(0o755)
|
227
|
+
return True
|
228
|
+
|
229
|
+
|
230
|
+
def testlib_grading_input() -> GradingFileInput:
|
231
|
+
return GradingFileInput(src=get_testlib(), dest=pathlib.Path('testlib.h'))
|
232
|
+
|
233
|
+
|
234
|
+
def jngen_grading_input() -> GradingFileInput:
|
235
|
+
return GradingFileInput(src=get_jngen(), dest=pathlib.Path('jngen.h'))
|
236
|
+
|
237
|
+
|
238
|
+
def _expand_part(part: str, sandbox: SandboxBase) -> List[str]:
|
239
|
+
part = part.strip()
|
240
|
+
if part.startswith('@glob:'):
|
241
|
+
return [shlex.quote(str(path)) for path in sandbox.glob(part[6:])]
|
242
|
+
return [part]
|
243
|
+
|
244
|
+
|
245
|
+
def _split_and_expand(command: str, sandbox: SandboxBase) -> List[str]:
|
246
|
+
res = []
|
247
|
+
parts = shlex.split(command.format(memory=sandbox.params.address_space))
|
248
|
+
for part in parts:
|
249
|
+
res.extend(_expand_part(part, sandbox))
|
250
|
+
return res
|
251
|
+
|
252
|
+
|
253
|
+
def _is_cpp_command(exe_command: str) -> bool:
|
254
|
+
return exe_command.endswith('g++') or exe_command.endswith('clang++')
|
255
|
+
|
256
|
+
|
257
|
+
def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]:
|
258
|
+
cmds = shlex.split(command)
|
259
|
+
if not cmds:
|
260
|
+
return None
|
261
|
+
exe = cmds[0]
|
262
|
+
|
263
|
+
if not _is_cpp_command(exe):
|
264
|
+
return None
|
265
|
+
|
266
|
+
output = subprocess.run([exe, '-v'], capture_output=True)
|
267
|
+
if output.returncode != 0:
|
268
|
+
console.print('[error]Failed to get g++/clang compiler version.[/error]')
|
269
|
+
return None
|
270
|
+
lines = output.stderr.decode().splitlines()
|
271
|
+
if not lines:
|
272
|
+
return None
|
273
|
+
# Check the first line for `clang`.
|
274
|
+
if 'clang' not in lines[0]:
|
275
|
+
return None
|
276
|
+
|
277
|
+
bits = get_bits_stdcpp()
|
278
|
+
return GradingFileInput(src=bits, dest=pathlib.Path('bits/stdc++.h'))
|
279
|
+
|
280
|
+
|
281
|
+
def _maybe_get_bits_stdcpp_for_commands(
|
282
|
+
commands: List[str],
|
283
|
+
) -> Optional[GradingFileInput]:
|
284
|
+
for command in commands:
|
285
|
+
res = _maybe_get_bits_stdcpp_for_clang(command)
|
286
|
+
if res is not None:
|
287
|
+
return res
|
288
|
+
return None
|
289
|
+
|
290
|
+
|
291
|
+
def compile(
|
292
|
+
commands: List[str],
|
293
|
+
params: SandboxParams,
|
294
|
+
sandbox: SandboxBase,
|
295
|
+
artifacts: GradingArtifacts,
|
296
|
+
) -> bool:
|
297
|
+
bits_artifact = _maybe_get_bits_stdcpp_for_commands(commands)
|
298
|
+
if bits_artifact is not None:
|
299
|
+
_process_input_artifacts(GradingArtifacts(inputs=[bits_artifact]), sandbox)
|
300
|
+
_process_input_artifacts(artifacts, sandbox)
|
301
|
+
|
302
|
+
if not commands:
|
303
|
+
# Code does not need preprocessing of any kind.
|
304
|
+
return True
|
305
|
+
|
306
|
+
logs: List[PreprocessLog] = []
|
307
|
+
sandbox.set_params(params)
|
308
|
+
|
309
|
+
for i, command in enumerate(commands):
|
310
|
+
cmd = _split_and_expand(command, sandbox)
|
311
|
+
stdout_file = pathlib.PosixPath(f'compile-{i}.stdout')
|
312
|
+
stderr_file = pathlib.PosixPath(f'compile-{i}.stderr')
|
313
|
+
sandbox.params.set_stdall(stdout=stdout_file, stderr=stderr_file)
|
314
|
+
|
315
|
+
if bits_artifact is not None and _is_cpp_command(cmd[0]):
|
316
|
+
# Include from sandbox directory to import bits/stdc++.h.
|
317
|
+
cmd.append('-I.')
|
318
|
+
|
319
|
+
if not sandbox.execute_without_std(cmd):
|
320
|
+
console.print(
|
321
|
+
'[error]Sandbox crashed while processing command:[/error]',
|
322
|
+
utils.highlight_json_obj(cmd),
|
323
|
+
'[error]and logs[/error]',
|
324
|
+
sandbox.debug_message(),
|
325
|
+
)
|
326
|
+
return False
|
327
|
+
|
328
|
+
std_outputs = [
|
329
|
+
sandbox.get_file_to_string(stderr_file, maxlen=None)
|
330
|
+
if sandbox.file_exists(stderr_file)
|
331
|
+
else '<No stderr produced by command>',
|
332
|
+
sandbox.get_file_to_string(stdout_file, maxlen=None)
|
333
|
+
if sandbox.file_exists(stdout_file)
|
334
|
+
else '<No stdout produced by command>',
|
335
|
+
]
|
336
|
+
|
337
|
+
log = PreprocessLog(
|
338
|
+
cmd=cmd,
|
339
|
+
exitcode=sandbox.get_exit_code(),
|
340
|
+
log='\n'.join(std_outputs),
|
341
|
+
)
|
342
|
+
logs.append(log)
|
343
|
+
|
344
|
+
if log.exitcode != 0:
|
345
|
+
break
|
346
|
+
|
347
|
+
if logs and logs[-1].exitcode != 0:
|
348
|
+
console.print(
|
349
|
+
'Preprocessing [error]failed[/error] with command',
|
350
|
+
utils.highlight_json_obj(logs[-1].cmd),
|
351
|
+
)
|
352
|
+
console.print(f'Exit code: [error]{logs[-1].exitcode}[/error]')
|
353
|
+
console.print(Text.from_ansi(logs[-1].log), style='default')
|
354
|
+
return False
|
355
|
+
|
356
|
+
return _process_output_artifacts(artifacts, sandbox)
|
357
|
+
|
358
|
+
|
359
|
+
def run(
|
360
|
+
command: str,
|
361
|
+
params: SandboxParams,
|
362
|
+
sandbox: SandboxBase,
|
363
|
+
artifacts: GradingArtifacts,
|
364
|
+
metadata: Optional[RunLogMetadata] = None,
|
365
|
+
) -> Optional[RunLog]:
|
366
|
+
_process_input_artifacts(artifacts, sandbox)
|
367
|
+
cmd = _split_and_expand(command, sandbox)
|
368
|
+
sandbox.set_params(params)
|
369
|
+
|
370
|
+
if not sandbox.execute_without_std(cmd):
|
371
|
+
console.print(
|
372
|
+
'[error]Sandbox crashed while processing command:[/error]',
|
373
|
+
utils.highlight_json_obj(cmd),
|
374
|
+
'[error]and logs[/error]',
|
375
|
+
sandbox.debug_message(),
|
376
|
+
)
|
377
|
+
return None
|
378
|
+
|
379
|
+
if not _process_output_artifacts(artifacts, sandbox):
|
380
|
+
return None
|
381
|
+
|
382
|
+
execution_time = sandbox.get_execution_time()
|
383
|
+
if execution_time is not None and (
|
384
|
+
sandbox.get_exit_status() == SandboxBase.EXIT_TIMEOUT
|
385
|
+
or sandbox.get_exit_status() == SandboxBase.EXIT_TIMEOUT_WALL
|
386
|
+
):
|
387
|
+
execution_time = max(execution_time, (params.timeout or 0.0) / 1000)
|
388
|
+
|
389
|
+
run_log = RunLog(
|
390
|
+
exitcode=sandbox.get_exit_code(),
|
391
|
+
exitstatus=sandbox.get_exit_status(),
|
392
|
+
time=sandbox.get_execution_time(),
|
393
|
+
memory=sandbox.get_memory_used(),
|
394
|
+
metadata=metadata,
|
395
|
+
)
|
396
|
+
if artifacts.logs is not None:
|
397
|
+
artifacts.logs.run = run_log.model_copy()
|
398
|
+
return run_log
|
399
|
+
|
400
|
+
|
401
|
+
def _normalize_checked_words(s: str) -> Tuple[str, ...]:
|
402
|
+
return tuple(s.split())
|
403
|
+
|
404
|
+
|
405
|
+
def _wcmp_check(expected: str, output: str) -> Outcome:
|
406
|
+
if _normalize_checked_words(expected) == _normalize_checked_words(output):
|
407
|
+
return Outcome.ACCEPTED
|
408
|
+
|
409
|
+
return Outcome.WRONG_ANSWER
|
410
|
+
|
411
|
+
|
412
|
+
def get_checker_sandbox_params() -> SandboxParams:
|
413
|
+
params = SandboxParams(
|
414
|
+
max_processes=None,
|
415
|
+
preserve_env=True,
|
416
|
+
)
|
417
|
+
params.add_mapped_directory(pathlib.Path('/usr'))
|
418
|
+
params.add_mapped_directory(pathlib.Path('/etc'))
|
419
|
+
return params
|
420
|
+
|
421
|
+
|
422
|
+
def _check(
|
423
|
+
sandbox: SandboxBase,
|
424
|
+
testcase: TestcaseIO,
|
425
|
+
output_path: pathlib.Path,
|
426
|
+
should_use_python_checker: bool = False,
|
427
|
+
) -> CheckerResult:
|
428
|
+
if testcase.output is None:
|
429
|
+
# No output to compare.
|
430
|
+
return CheckerResult(outcome=Outcome.ACCEPTED)
|
431
|
+
|
432
|
+
if should_use_python_checker:
|
433
|
+
# Use default wcmp checker.
|
434
|
+
expected = testcase.output.read_text()
|
435
|
+
output = output_path.read_text()
|
436
|
+
|
437
|
+
return CheckerResult(outcome=_wcmp_check(expected, output))
|
438
|
+
|
439
|
+
sandbox.params.set_stdall(
|
440
|
+
stdin=None,
|
441
|
+
stdout=pathlib.PosixPath('stdout.txt'),
|
442
|
+
stderr=pathlib.PosixPath('stderr.txt'),
|
443
|
+
)
|
444
|
+
|
445
|
+
sandbox.create_file_from_string(
|
446
|
+
pathlib.PosixPath('expected.txt'), testcase.output.read_text(), override=True
|
447
|
+
)
|
448
|
+
sandbox.create_file_from_string(
|
449
|
+
pathlib.PosixPath('output.txt'), output_path.read_text(), override=True
|
450
|
+
)
|
451
|
+
sandbox.create_file_from_string(
|
452
|
+
pathlib.PosixPath('input.txt'),
|
453
|
+
testcase.input.read_text() if testcase.input is not None else '',
|
454
|
+
override=True,
|
455
|
+
)
|
456
|
+
|
457
|
+
if not sandbox.execute_without_std(
|
458
|
+
['./checker', 'input.txt', 'output.txt', 'expected.txt'],
|
459
|
+
):
|
460
|
+
console.print(
|
461
|
+
'[error]Sandbox crashed while running checker.[/error]',
|
462
|
+
'[error]and logs[/error]',
|
463
|
+
sandbox.debug_message(),
|
464
|
+
)
|
465
|
+
return CheckerResult(outcome=Outcome.INTERNAL_ERROR)
|
466
|
+
|
467
|
+
checker_stderr = sandbox.get_file_to_string(
|
468
|
+
pathlib.PosixPath('stderr.txt'), maxlen=None
|
469
|
+
)
|
470
|
+
if sandbox.get_exit_code() in [1, 2]:
|
471
|
+
return CheckerResult(outcome=Outcome.WRONG_ANSWER, message=checker_stderr)
|
472
|
+
if sandbox.get_exit_code() == 3:
|
473
|
+
return CheckerResult(outcome=Outcome.JUDGE_FAILED, message=checker_stderr)
|
474
|
+
return CheckerResult(outcome=Outcome.ACCEPTED, message=checker_stderr)
|
475
|
+
|
476
|
+
|
477
|
+
# Always assume a `checker` executable in the sandbox if should use checker.
|
478
|
+
def evaluate(
|
479
|
+
sandbox: SandboxBase,
|
480
|
+
testcase: TestcaseIO,
|
481
|
+
log: Optional[TestcaseLog],
|
482
|
+
artifacts: GradingArtifacts,
|
483
|
+
should_use_python_checker: bool = False,
|
484
|
+
) -> Evaluation:
|
485
|
+
if log is None:
|
486
|
+
return Evaluation(
|
487
|
+
testcase=testcase,
|
488
|
+
log=TestcaseLog(),
|
489
|
+
result=CheckerResult(outcome=Outcome.INTERNAL_ERROR),
|
490
|
+
)
|
491
|
+
if log.exitstatus != SandboxBase.EXIT_OK:
|
492
|
+
return Evaluation(
|
493
|
+
testcase=testcase,
|
494
|
+
log=log,
|
495
|
+
result=CheckerResult(outcome=Outcome.RUNTIME_ERROR),
|
496
|
+
)
|
497
|
+
|
498
|
+
if not testcase.output:
|
499
|
+
# No output to compare.
|
500
|
+
return Evaluation(
|
501
|
+
testcase=testcase, log=log, result=CheckerResult(outcome=Outcome.ACCEPTED)
|
502
|
+
)
|
503
|
+
|
504
|
+
_process_input_artifacts(artifacts, sandbox)
|
505
|
+
if log.stdout_absolute_path is None:
|
506
|
+
return Evaluation(
|
507
|
+
testcase=testcase,
|
508
|
+
log=log,
|
509
|
+
result=CheckerResult(outcome=Outcome.INTERNAL_ERROR, message='No output'),
|
510
|
+
)
|
511
|
+
|
512
|
+
checker_result = _check(
|
513
|
+
sandbox,
|
514
|
+
testcase,
|
515
|
+
log.stdout_absolute_path,
|
516
|
+
should_use_python_checker=should_use_python_checker,
|
517
|
+
)
|
518
|
+
return Evaluation(
|
519
|
+
testcase=testcase,
|
520
|
+
log=log,
|
521
|
+
result=checker_result,
|
522
|
+
)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
from rbx.grading import steps
|
4
|
+
from rbx.grading.caching import DependencyCache, NoCacheException
|
5
|
+
from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
|
6
|
+
from rbx.grading.steps import (
|
7
|
+
GradingArtifacts,
|
8
|
+
GradingLogsHolder,
|
9
|
+
RunLog,
|
10
|
+
RunLogMetadata,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
def compile(
|
15
|
+
commands: List[str],
|
16
|
+
params: SandboxParams,
|
17
|
+
sandbox: SandboxBase,
|
18
|
+
artifacts: GradingArtifacts,
|
19
|
+
dependency_cache: DependencyCache,
|
20
|
+
):
|
21
|
+
ok = True
|
22
|
+
with dependency_cache(
|
23
|
+
commands, [artifacts], params.get_cacheable_params()
|
24
|
+
) as is_cached:
|
25
|
+
if not is_cached and not steps.compile(
|
26
|
+
commands=commands,
|
27
|
+
params=params,
|
28
|
+
artifacts=artifacts,
|
29
|
+
sandbox=sandbox,
|
30
|
+
):
|
31
|
+
ok = False
|
32
|
+
raise NoCacheException()
|
33
|
+
|
34
|
+
return ok
|
35
|
+
|
36
|
+
|
37
|
+
def run(
|
38
|
+
command: str,
|
39
|
+
params: SandboxParams,
|
40
|
+
sandbox: SandboxBase,
|
41
|
+
artifacts: GradingArtifacts,
|
42
|
+
dependency_cache: DependencyCache,
|
43
|
+
metadata: Optional[RunLogMetadata] = None,
|
44
|
+
) -> Optional[RunLog]:
|
45
|
+
artifacts.logs = GradingLogsHolder()
|
46
|
+
|
47
|
+
with dependency_cache(
|
48
|
+
[command], [artifacts], params.get_cacheable_params()
|
49
|
+
) as is_cached:
|
50
|
+
if not is_cached:
|
51
|
+
steps.run(
|
52
|
+
command=command,
|
53
|
+
params=params,
|
54
|
+
artifacts=artifacts,
|
55
|
+
sandbox=sandbox,
|
56
|
+
metadata=metadata,
|
57
|
+
)
|
58
|
+
|
59
|
+
return artifacts.logs.run
|