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.
Files changed (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. 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