rbx.cp 0.13.3__py3-none-any.whl → 0.13.5__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 (73) hide show
  1. rbx/annotations.py +5 -5
  2. rbx/box/checkers.py +26 -22
  3. rbx/box/cli.py +0 -4
  4. rbx/box/code.py +27 -80
  5. rbx/box/contest/build_contest_statements.py +16 -3
  6. rbx/box/contest/schema.py +1 -2
  7. rbx/box/environment.py +16 -6
  8. rbx/box/fields.py +25 -1
  9. rbx/box/generators.py +31 -5
  10. rbx/box/global_package.py +6 -2
  11. rbx/box/header.py +31 -11
  12. rbx/box/package.py +3 -15
  13. rbx/box/presets/__init__.py +2 -2
  14. rbx/box/schema.py +4 -25
  15. rbx/box/setter_config.py +11 -0
  16. rbx/box/solutions.py +12 -4
  17. rbx/box/statements/build_statements.py +5 -1
  18. rbx/box/statements/builders.py +7 -7
  19. rbx/box/statements/schema.py +11 -2
  20. rbx/box/tasks.py +9 -4
  21. rbx/box/testcase_utils.py +2 -0
  22. rbx/box/testing/__init__.py +0 -0
  23. rbx/box/testing/testing_package.py +246 -0
  24. rbx/box/testing/testing_preset.py +36 -0
  25. rbx/box/testing/testing_shared.py +81 -0
  26. rbx/box/ui/screens/run_explorer.py +0 -8
  27. rbx/box/ui/utils/run_ui.py +7 -3
  28. rbx/box/ui/widgets/test_output_box.py +1 -1
  29. rbx/box/validators.py +5 -2
  30. rbx/grading/caching.py +67 -16
  31. rbx/grading/judge/program.py +268 -0
  32. rbx/grading/judge/sandbox.py +30 -193
  33. rbx/grading/judge/sandboxes/stupid_sandbox.py +232 -241
  34. rbx/grading/judge/sandboxes/tee.py +31 -0
  35. rbx/grading/steps.py +87 -199
  36. rbx/grading/steps_with_caching.py +15 -6
  37. rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
  38. rbx/resources/presets/default/shared/contest_template.rbx.tex +1 -1
  39. rbx/resources/presets/default/shared/problem_template.rbx.tex +5 -1
  40. rbx/resources/templates/rbx.h +43 -2
  41. rbx/testing_utils.py +8 -1
  42. rbx/utils.py +59 -1
  43. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/METADATA +2 -1
  44. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/RECORD +47 -67
  45. rbx/box/conftest.py +0 -42
  46. rbx/box/generators_test.py +0 -67
  47. rbx/box/lazy_importing_test.py +0 -25
  48. rbx/box/solutions_test.py +0 -47
  49. rbx/box/validators_test.py +0 -15
  50. rbx/checker.py +0 -128
  51. rbx/clone.py +0 -197
  52. rbx/conftest.py +0 -38
  53. rbx/create.py +0 -37
  54. rbx/edit.py +0 -24
  55. rbx/grading/conftest.py +0 -33
  56. rbx/grading/judge/sandboxes/isolate.py +0 -695
  57. rbx/grading/judge/testiso.py +0 -54
  58. rbx/grading/steps_with_caching_run_test.py +0 -707
  59. rbx/grading_utils.py +0 -148
  60. rbx/hydration.py +0 -101
  61. rbx/main.py +0 -118
  62. rbx/metadata.py +0 -105
  63. rbx/resources/envs/isolate.rbx.yml +0 -36
  64. rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
  65. rbx/run.py +0 -45
  66. rbx/schema.py +0 -64
  67. rbx/submit.py +0 -61
  68. rbx/test.py +0 -349
  69. rbx/testcase.py +0 -70
  70. rbx/testcase_rendering.py +0 -79
  71. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/LICENSE +0 -0
  72. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/WHEEL +0 -0
  73. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/entry_points.txt +0 -0
rbx/annotations.py CHANGED
@@ -7,7 +7,7 @@ import typer
7
7
  import typer.core
8
8
  from typing_extensions import Annotated
9
9
 
10
- from rbx import config, metadata
10
+ from rbx import config
11
11
  from rbx.config import get_config
12
12
 
13
13
 
@@ -21,10 +21,10 @@ def _get_language_default():
21
21
 
22
22
  def _get_problem_options():
23
23
  options = set()
24
- all_problems = metadata.find_problems()
25
- for problem in all_problems:
26
- options.add(problem.code)
27
- options.update(problem.aliases)
24
+ # all_problems = metadata.find_problems()
25
+ # for problem in all_problems:
26
+ # options.add(problem.code)
27
+ # options.update(problem.aliases)
28
28
  return sorted(options)
29
29
 
30
30
 
rbx/box/checkers.py CHANGED
@@ -1,12 +1,10 @@
1
1
  import pathlib
2
- import signal
3
2
  from typing import List, Optional
4
3
 
5
4
  import typer
6
5
 
7
6
  from rbx import console
8
- from rbx.box import package
9
- from rbx.box.code import SanitizationLevel, compile_item, run_item
7
+ from rbx.box import code, package
10
8
  from rbx.box.schema import Testcase
11
9
  from rbx.grading.judge.sandbox import SandboxBase
12
10
  from rbx.grading.steps import (
@@ -28,7 +26,7 @@ def compile_checker(progress: Optional[StatusProgress] = None) -> str:
28
26
  progress.update('Compiling checker...')
29
27
 
30
28
  try:
31
- digest = compile_item(checker, sanitized=SanitizationLevel.PREFER)
29
+ digest = code.compile_item(checker, sanitized=code.SanitizationLevel.PREFER)
32
30
  except Exception as e:
33
31
  console.console.print('[error]Failed compiling checker[/error]')
34
32
  raise typer.Exit(1) from e
@@ -46,7 +44,7 @@ def compile_interactor(progress: Optional[StatusProgress] = None) -> str:
46
44
  progress.update('Compiling interactor...')
47
45
 
48
46
  try:
49
- digest = compile_item(interactor, sanitized=SanitizationLevel.PREFER)
47
+ digest = code.compile_item(interactor, sanitized=code.SanitizationLevel.PREFER)
50
48
  except Exception as e:
51
49
  console.console.print('[error]Failed compiling interactor.[/error]')
52
50
  raise typer.Exit(1) from e
@@ -164,6 +162,14 @@ def process_checker_run_log(
164
162
 
165
163
  if checker_run_log is None:
166
164
  return CheckerResult(outcome=Outcome.INTERNAL_ERROR)
165
+ if checker_run_log.exitstatus not in [
166
+ SandboxBase.EXIT_OK,
167
+ SandboxBase.EXIT_NONZERO_RETURN,
168
+ ]:
169
+ return CheckerResult(
170
+ outcome=Outcome.JUDGE_FAILED,
171
+ message=f'checker failed with exit status {checker_run_log.exitstatus}: {message}',
172
+ )
167
173
  if not _is_checker_exitcode(checker_run_log.exitcode):
168
174
  return CheckerResult(
169
175
  outcome=Outcome.JUDGE_FAILED,
@@ -196,13 +202,17 @@ async def _check(
196
202
  if result.outcome != Outcome.ACCEPTED:
197
203
  return _convert_tle(result, run_log)
198
204
 
199
- pkg = package.find_problem_package_or_die()
200
- output_size = program_output.stat().st_size
201
- if output_size > pkg.outputLimit * 1024:
202
- return CheckerResult(
203
- outcome=Outcome.OUTPUT_LIMIT_EXCEEDED,
204
- message=f'Output size {pkg.outputLimit}kb, limit is {output_size // 1024}kb.',
205
- )
205
+ if (
206
+ run_log is not None
207
+ and run_log.metadata is not None
208
+ and run_log.metadata.limits.output is not None
209
+ ):
210
+ output_size = program_output.stat().st_size
211
+ if output_size > run_log.metadata.limits.output * 1024:
212
+ return CheckerResult(
213
+ outcome=Outcome.OUTPUT_LIMIT_EXCEEDED,
214
+ message=f'Output size {run_log.metadata.limits.output}kb, limit is {output_size // 1024}kb.',
215
+ )
206
216
 
207
217
  error = DigestHolder()
208
218
  inputs = [
@@ -219,14 +229,14 @@ async def _check(
219
229
  dest=pathlib.PosixPath('output.txt'),
220
230
  ),
221
231
  ]
222
- checker_run_log = await run_item(
232
+ checker_run_log = await code.run_item(
223
233
  package.get_checker(),
224
234
  DigestOrSource.create(checker_digest),
225
235
  stderr=DigestOrDest.create(error),
226
236
  inputs=inputs,
227
237
  extra_args='input.txt output.txt expected.txt',
228
238
  )
229
- message = package.get_digest_as_string(error.value or '') or ''
239
+ message = package.get_digest_as_string(error.value) or ''
230
240
 
231
241
  processed_checker_result = process_checker_run_log(checker_run_log, message)
232
242
  if processed_checker_result.outcome == Outcome.INTERNAL_ERROR:
@@ -237,6 +247,7 @@ async def _check(
237
247
  console.console.print(
238
248
  f'[error]Summary:[/error] {checker_run_log.get_summary()}'
239
249
  )
250
+ console.console.print(f'[error]Message:[/error] {message}')
240
251
  console.console.print(
241
252
  f'[error]Testcase input:[/error] [item]{testcase.inputPath}[/item]'
242
253
  )
@@ -334,14 +345,7 @@ async def check_communication(
334
345
  if (
335
346
  interactor_run_log is not None
336
347
  and run_log is not None
337
- and (
338
- run_log.exitcode == -signal.SIGPIPE
339
- or run_log.exitstatus == SandboxBase.EXIT_TERMINATED
340
- or (
341
- run_log.exitstatus == SandboxBase.EXIT_NONZERO_RETURN
342
- and not _is_testlib_eof(interactor_stderr.read_text())
343
- )
344
- )
348
+ and (interactor_run_log.exitindex < run_log.exitindex)
345
349
  ):
346
350
  result = _check_interactor()
347
351
  if result is not None and result.outcome != Outcome.ACCEPTED:
rbx/box/cli.py CHANGED
@@ -128,7 +128,6 @@ def main(
128
128
  capture: bool = typer.Option(
129
129
  True,
130
130
  '--nocapture',
131
- flag_value=False,
132
131
  help='Whether to save extra logs and outputs from interactive solutions.',
133
132
  ),
134
133
  profile: bool = typer.Option(
@@ -259,7 +258,6 @@ async def run(
259
258
  check: bool = typer.Option(
260
259
  True,
261
260
  '--nocheck',
262
- flag_value=False,
263
261
  help='Whether to not build outputs for tests and run checker.',
264
262
  ),
265
263
  detailed: bool = typer.Option(
@@ -432,7 +430,6 @@ async def time(
432
430
  check: bool = typer.Option(
433
431
  True,
434
432
  '--nocheck',
435
- flag_value=False,
436
433
  help='Whether to not build outputs for tests and run checker.',
437
434
  ),
438
435
  detailed: bool = typer.Option(
@@ -488,7 +485,6 @@ async def irun(
488
485
  check: bool = typer.Option(
489
486
  True,
490
487
  '--nocheck',
491
- flag_value=False,
492
488
  help='Whether to not build outputs for tests and run checker.',
493
489
  ),
494
490
  generator: Optional[str] = typer.Option(
rbx/box/code.py CHANGED
@@ -12,7 +12,7 @@ import rich.text
12
12
  import typer
13
13
  from pydantic import BaseModel
14
14
 
15
- from rbx import console, utils
15
+ from rbx import console
16
16
  from rbx.box import download, global_package, package, setter_config, state
17
17
  from rbx.box.environment import (
18
18
  CompilationConfig,
@@ -48,6 +48,8 @@ from rbx.grading.steps import (
48
48
  maybe_get_bits_stdcpp_for_commands,
49
49
  )
50
50
 
51
+ MERGED_CAPTURE_FILENAME = 'merged_capture.pio'
52
+
51
53
 
52
54
  class SanitizationLevel(Enum):
53
55
  NONE = 0
@@ -207,6 +209,9 @@ def _format_stack_limit(limit: int) -> str:
207
209
 
208
210
 
209
211
  def _check_stack_limit():
212
+ cfg = setter_config.get_setter_config()
213
+ if not cfg.judging.check_stack:
214
+ return
210
215
  if not state.STATE.run_through_cli:
211
216
  return
212
217
  soft, hard = resource.RLIM_INFINITY, resource.RLIM_INFINITY
@@ -261,52 +266,6 @@ class PreparedRun:
261
266
  metadata: RunLogMetadata
262
267
 
263
268
 
264
- @dataclasses.dataclass
265
- class CaptureSpec:
266
- prefix: str
267
- output: Optional[DigestOrDest] = None
268
- merged_capture: Optional[pathlib.Path] = None
269
-
270
-
271
- def _prepare_for_communication(
272
- run: PreparedRun,
273
- stdin: pathlib.Path,
274
- stdout: pathlib.Path,
275
- reverse_io: bool = False,
276
- capture: Optional[CaptureSpec] = None,
277
- ):
278
- run.sandbox_params.set_stdio(
279
- stdin=stdin,
280
- stdout=stdout,
281
- )
282
- run.sandbox_params.reverse_io = reverse_io
283
- if capture is not None:
284
- run.sandbox_params.timeit_prefix = capture.prefix
285
-
286
- if capture.output is not None:
287
- output_path = PosixPath('capture')
288
- run.sandbox_params.timeit_dups['do'].append(output_path)
289
-
290
- run.artifacts.outputs.append(
291
- GradingFileOutput(
292
- src=output_path,
293
- **capture.output.expand(),
294
- touch=True,
295
- )
296
- )
297
-
298
- if capture.merged_capture is not None:
299
- merged_output_path = utils.abspath(package.get_merged_capture_path())
300
- run.sandbox_params.timeit_dups['Do'].append(merged_output_path)
301
-
302
- run.artifacts.outputs.append(
303
- GradingFileOutput(
304
- src=merged_output_path,
305
- dest=capture.merged_capture,
306
- )
307
- )
308
-
309
-
310
269
  def _prepare_run(
311
270
  code: CodeItem,
312
271
  executable: DigestOrSource,
@@ -318,12 +277,13 @@ def _prepare_run(
318
277
  extra_args: Optional[str] = None,
319
278
  extra_config: Optional[ExecutionConfig] = None,
320
279
  retry_index: Optional[int] = None,
280
+ file_prefix: Optional[str] = None,
321
281
  ):
322
282
  language = find_language_name(code)
323
283
  execution_options = get_execution_config(language)
324
284
  if extra_config is not None:
325
285
  execution_options = merge_execution_configs([execution_options, extra_config])
326
- file_mapping = get_file_mapping(language)
286
+ file_mapping = get_file_mapping(language, file_prefix)
327
287
  sandbox_params = get_sandbox_params_from_config(execution_options.sandbox)
328
288
 
329
289
  # Sanitization parameters.
@@ -739,6 +699,7 @@ async def run_item(
739
699
  class CommunicationItem:
740
700
  code: CodeItem
741
701
  executable: DigestOrSource
702
+ file_prefix: str
742
703
  stderr: Optional[DigestOrDest] = None
743
704
  inputs: Optional[List[GradingFileInput]] = None
744
705
  outputs: Optional[List[GradingFileOutput]] = None
@@ -750,21 +711,22 @@ class CommunicationItem:
750
711
  return _prepare_run(
751
712
  self.code,
752
713
  self.executable,
714
+ stdout=self.capture,
753
715
  stderr=self.stderr,
754
716
  inputs=self.inputs,
755
717
  outputs=self.outputs,
756
718
  extra_args=self.extra_args,
757
719
  extra_config=self.extra_config,
720
+ file_prefix=self.file_prefix,
758
721
  )
759
722
 
760
723
 
761
724
  async def run_communication(
762
725
  interactor: CommunicationItem,
763
726
  solution: CommunicationItem,
764
- merged_capture: Optional[pathlib.Path] = None,
727
+ merged_capture: Optional[DigestOrDest] = None,
765
728
  retry_index: Optional[int] = None,
766
729
  ):
767
- fifo_in, fifo_out = package.get_fifos()
768
730
  interactor_prepared = interactor.prepare()
769
731
  solution_prepared = solution.prepare()
770
732
 
@@ -772,48 +734,30 @@ async def run_communication(
772
734
  interactor_prepared.metadata.retryIndex = retry_index
773
735
  solution_prepared.metadata.retryIndex = retry_index
774
736
 
775
- interactor_prefix = '<'
776
- solution_prefix = '>'
737
+ grading_artifacts = GradingArtifacts()
738
+ grading_artifacts.inputs.extend(interactor_prepared.artifacts.inputs)
739
+ grading_artifacts.outputs.extend(interactor_prepared.artifacts.outputs)
740
+ grading_artifacts.inputs.extend(solution_prepared.artifacts.inputs)
741
+ grading_artifacts.outputs.extend(solution_prepared.artifacts.outputs)
777
742
 
743
+ merged_capture_path: Optional[pathlib.Path] = None
778
744
  if merged_capture is not None:
779
- package.get_merged_capture_path().write_text(
780
- f'{interactor_prefix}\n{solution_prefix}\n'
745
+ merged_capture_path = pathlib.Path(MERGED_CAPTURE_FILENAME)
746
+ grading_artifacts.outputs.append(
747
+ GradingFileOutput(
748
+ src=merged_capture_path,
749
+ **merged_capture.expand(),
750
+ )
781
751
  )
782
752
 
783
- _prepare_for_communication(
784
- interactor_prepared,
785
- fifo_out,
786
- fifo_in,
787
- capture=CaptureSpec(
788
- prefix=interactor_prefix,
789
- output=interactor.capture,
790
- merged_capture=merged_capture,
791
- ),
792
- )
793
- _prepare_for_communication(
794
- solution_prepared,
795
- fifo_in,
796
- fifo_out,
797
- reverse_io=True,
798
- capture=CaptureSpec(
799
- prefix=solution_prefix,
800
- output=solution.capture,
801
- merged_capture=merged_capture,
802
- ),
803
- )
804
-
805
753
  interactor_run_params = steps.CoordinatedRunParams(
806
754
  command=interactor_prepared.command,
807
755
  params=interactor_prepared.sandbox_params,
808
- sandbox=package.get_singleton_interactor_sandbox(),
809
- artifacts=interactor_prepared.artifacts,
810
756
  metadata=interactor_prepared.metadata,
811
757
  )
812
758
  solution_run_params = steps.CoordinatedRunParams(
813
759
  command=solution_prepared.command,
814
760
  params=solution_prepared.sandbox_params,
815
- sandbox=package.get_singleton_sandbox(),
816
- artifacts=solution_prepared.artifacts,
817
761
  metadata=solution_prepared.metadata,
818
762
  )
819
763
 
@@ -825,5 +769,8 @@ async def run_communication(
825
769
  return await steps_with_caching.run_coordinated(
826
770
  interactor_run_params,
827
771
  solution_run_params,
772
+ sandbox=package.get_singleton_sandbox(),
773
+ artifacts=grading_artifacts,
828
774
  dependency_cache=package.get_dependency_cache(),
775
+ merged_capture=merged_capture_path,
829
776
  )
@@ -10,6 +10,7 @@ from rbx import console, testing_utils, utils
10
10
  from rbx.box import cd, package
11
11
  from rbx.box.contest.contest_package import get_problems
12
12
  from rbx.box.contest.schema import Contest, ContestProblem, ContestStatement
13
+ from rbx.box.fields import Primitive
13
14
  from rbx.box.formatting import href
14
15
  from rbx.box.schema import Package, Testcase
15
16
  from rbx.box.statements import build_statements, latex
@@ -72,14 +73,21 @@ def get_statement_builder_problems(
72
73
 
73
74
 
74
75
  def get_statement_builder_contest(
76
+ contest: Contest,
75
77
  statement: ContestStatement,
76
78
  extracted_problems: List[ExtractedProblem],
79
+ custom_vars: Optional[Dict[str, Primitive]] = None,
77
80
  ) -> StatementBuilderContest:
78
81
  return StatementBuilderContest(
79
82
  title=statement.title,
80
83
  location=statement.location,
81
84
  date=statement.date,
82
85
  problems=get_statement_builder_problems(extracted_problems),
86
+ vars={
87
+ **contest.expanded_vars,
88
+ **statement.expanded_vars,
89
+ **(custom_vars or {}),
90
+ },
83
91
  )
84
92
 
85
93
 
@@ -241,10 +249,13 @@ def build_contest_only(
241
249
  languages=get_environment_languages_for_statement(),
242
250
  params=params,
243
251
  root=pathlib.Path(td),
252
+ ),
253
+ item=get_statement_builder_contest(
254
+ contest,
255
+ statement,
256
+ extracted_problems,
244
257
  custom_vars=custom_vars,
245
- vars={**contest.expanded_vars, **statement.expanded_vars},
246
258
  ),
247
- item=get_statement_builder_contest(statement, extracted_problems),
248
259
  verbose=False,
249
260
  )
250
261
 
@@ -326,7 +337,9 @@ def build_statement_rooted(
326
337
  last_content = joiner.build(
327
338
  last_content,
328
339
  context=joiner_context,
329
- contest=get_statement_builder_contest(statement, extracted_problems),
340
+ contest=get_statement_builder_contest(
341
+ contest, statement, extracted_problems, custom_vars=custom_vars
342
+ ),
330
343
  )
331
344
  last_output = joiner.output_type()
332
345
 
rbx/box/contest/schema.py CHANGED
@@ -3,8 +3,7 @@ from typing import Annotated, Dict, List, Optional
3
3
 
4
4
  from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
5
5
 
6
- from rbx.box.fields import FNameField, NameField
7
- from rbx.box.schema import Primitive, expand_var
6
+ from rbx.box.fields import FNameField, NameField, Primitive, expand_var
8
7
  from rbx.box.statements.expander import expand_statements
9
8
  from rbx.box.statements.schema import (
10
9
  ConversionStep,
rbx/box/environment.py CHANGED
@@ -10,7 +10,6 @@ from rbx import config, console, utils
10
10
  from rbx.box import presets
11
11
  from rbx.box.extensions import Extensions, LanguageExtensions
12
12
  from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
13
- from rbx.grading.judge.sandboxes.isolate import IsolateSandbox
14
13
  from rbx.grading.judge.sandboxes.stupid_sandbox import StupidSandbox
15
14
  from rbx.grading.limits import Limits
16
15
 
@@ -58,6 +57,12 @@ relative to the sandbox root.""",
58
57
  relative to the sandbox root.""",
59
58
  )
60
59
 
60
+ capture: str = Field(
61
+ default='capture',
62
+ description="""Path where to output the capture file after running the program,
63
+ relative to the sandbox root.""",
64
+ )
65
+
61
66
  compilable: str = Field(
62
67
  default='compilable',
63
68
  description="""Path where to copy the compilable file to before compiling the program,
@@ -232,7 +237,7 @@ execution config can be individually overridden in the language configuration.""
232
237
 
233
238
  sandbox: str = Field(
234
239
  default='stupid',
235
- description="""Identifier of the sandbox used by this environment (e.g. "stupid", "isolate")""",
240
+ description="""Identifier of the sandbox used by this environment (e.g. "stupid")""",
236
241
  )
237
242
 
238
243
  timing: TimingConfig = Field(
@@ -381,13 +386,20 @@ def get_execution_config(language: str) -> ExecutionConfig:
381
386
 
382
387
 
383
388
  @functools.cache
384
- def get_file_mapping(language: str) -> FileMapping:
389
+ def get_file_mapping(language: str, file_prefix: Optional[str] = None) -> FileMapping:
385
390
  environment = get_environment()
386
- return _merge_shallow_models(
391
+ mapping = _merge_shallow_models(
387
392
  FileMapping,
388
393
  environment.defaultFileMapping or FileMapping(),
389
394
  get_language(language).fileMapping or FileMapping(),
390
395
  )
396
+ if file_prefix is not None:
397
+ mapping.input = f'{file_prefix}_{mapping.input}'
398
+ mapping.output = f'{file_prefix}_{mapping.output}'
399
+ mapping.error = f'{file_prefix}_{mapping.error}'
400
+ mapping.compilable = f'{file_prefix}_{mapping.compilable}'
401
+ mapping.executable = f'{file_prefix}_{mapping.executable}'
402
+ return mapping
391
403
 
392
404
 
393
405
  @functools.cache
@@ -395,8 +407,6 @@ def get_sandbox_type() -> Type[SandboxBase]:
395
407
  used_sandbox = get_environment().sandbox
396
408
  if used_sandbox == 'stupid':
397
409
  return StupidSandbox
398
- if used_sandbox == 'isolate':
399
- return IsolateSandbox
400
410
  return StupidSandbox
401
411
 
402
412
 
rbx/box/fields.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TypeVar
1
+ from typing import Dict, TypeVar, Union
2
2
 
3
3
  from deepmerge import always_merger
4
4
  from pydantic import BaseModel, Field
@@ -33,3 +33,27 @@ def merge_pydantic_models(base: T, nxt: T) -> T:
33
33
  nxt_dict = nxt.model_dump(exclude_unset=True)
34
34
  merged_dict = always_merger.merge(base_dict, nxt_dict)
35
35
  return base.model_validate(merged_dict)
36
+
37
+
38
+ Primitive = Union[str, int, float, bool]
39
+
40
+
41
+ def expand_var(value: Primitive) -> Primitive:
42
+ if not isinstance(value, str):
43
+ return value
44
+ if value.startswith('\\'):
45
+ return value[1:]
46
+ if not value.startswith('py`') or not value.endswith('`'):
47
+ return value
48
+ res = eval(value[3:-1])
49
+ for supported_type in [str, int, float, bool]:
50
+ if isinstance(res, supported_type):
51
+ return res
52
+
53
+ raise TypeError(
54
+ f'Variable with backticks should evaluate to a primitive Python type: {value}'
55
+ )
56
+
57
+
58
+ def expand_vars(vars: Dict[str, Primitive]) -> Dict[str, Primitive]:
59
+ return {key: expand_var(value) for key, value in vars.items()}
rbx/box/generators.py CHANGED
@@ -63,13 +63,16 @@ def _warn_about_crlf(path: pathlib.Path):
63
63
 
64
64
 
65
65
  def _check_crlf(path: pathlib.Path):
66
+ should_fix = False
66
67
  with open(path, 'rb') as f:
67
68
  for line in f:
68
- if line.endswith(b'\r\n') or line.endswith(b'\n\r'):
69
+ if line.endswith(b'\r\n'):
69
70
  _warn_about_crlf(path)
71
+ should_fix = True
70
72
  break
71
73
 
72
- path.write_text(path.read_text().replace('\r', ''))
74
+ if should_fix:
75
+ path.write_text('\n'.join(path.read_text().splitlines()) + '\n')
73
76
 
74
77
 
75
78
  def _copy_testcase_over(
@@ -117,15 +120,25 @@ def _copy_testcase_output_over(
117
120
 
118
121
 
119
122
  def _copy_testcase_outputs_over(
120
- testcase: Testcase, dest: Testcase, pipes: bool = False, dry_run: bool = False
123
+ testcase: Testcase,
124
+ dest: Testcase,
125
+ pipes: bool = False,
126
+ only_pipes: bool = False,
127
+ dry_run: bool = False,
121
128
  ):
129
+ if only_pipes:
130
+ pipes = True
122
131
  assert dest.outputPath is not None
123
132
  if not dry_run:
124
133
  dest.outputPath.parent.mkdir(parents=True, exist_ok=True)
125
134
 
126
135
  has_copied = False
127
136
 
128
- if testcase.outputPath is not None and testcase.outputPath.is_file():
137
+ if (
138
+ not only_pipes
139
+ and testcase.outputPath is not None
140
+ and testcase.outputPath.is_file()
141
+ ):
129
142
  if not dry_run:
130
143
  _check_crlf(testcase.outputPath)
131
144
  shutil.copy(str(testcase.outputPath), str(dest.outputPath))
@@ -368,6 +381,7 @@ async def generate_output_for_testcase(
368
381
  main_solution_digest: str,
369
382
  testcase: Testcase,
370
383
  interactor_digest: Optional[str] = None,
384
+ capture_pipes: Optional[bool] = None,
371
385
  ):
372
386
  assert testcase.outputPath is not None
373
387
  testcase.inputPath.parent.mkdir(parents=True, exist_ok=True)
@@ -385,7 +399,7 @@ async def generate_output_for_testcase(
385
399
  interactor_digest=interactor_digest,
386
400
  use_retries=False,
387
401
  use_timelimit=False,
388
- capture_pipes=True,
402
+ capture_pipes=capture_pipes,
389
403
  )
390
404
 
391
405
  if eval.result.outcome.is_slow() and eval.result.no_tle_outcome == Outcome.ACCEPTED:
@@ -471,10 +485,22 @@ async def generate_outputs_for_testcases(
471
485
  raise typer.Exit(1)
472
486
 
473
487
  assert solution_digest is not None
488
+ capture_pipes = None
489
+ if (
490
+ pkg.type == TaskType.COMMUNICATION
491
+ and entry.metadata.copied_from is not None
492
+ ):
493
+ # If some pipe file is already specified, we don't need to capture the pipes
494
+ # when running the program.
495
+ capture_pipes = not _copy_testcase_outputs_over(
496
+ entry.metadata.copied_from, tc, only_pipes=True, dry_run=True
497
+ )
498
+
474
499
  await generate_output_for_testcase(
475
500
  solution_digest,
476
501
  tc,
477
502
  interactor_digest=interactor_digest,
503
+ capture_pipes=capture_pipes,
478
504
  )
479
505
  if entry.metadata.copied_from is not None:
480
506
  # Copy remaining pipe files.
rbx/box/global_package.py CHANGED
@@ -9,7 +9,7 @@ from rbx.grading.judge.sandbox import SandboxBase
9
9
  from rbx.grading.judge.sandboxes.stupid_sandbox import StupidSandbox
10
10
  from rbx.grading.judge.storage import FilesystemStorage, Storage
11
11
 
12
- CACHE_STEP_VERSION = 3
12
+ CACHE_STEP_VERSION = 4
13
13
 
14
14
 
15
15
  def get_cache_fingerprint() -> str:
@@ -29,9 +29,13 @@ def is_cache_valid(cache_dir: pathlib.Path) -> bool:
29
29
  return True
30
30
 
31
31
 
32
+ def get_global_cache_dir_path() -> pathlib.Path:
33
+ return get_app_path() / '.box'
34
+
35
+
32
36
  @functools.cache
33
37
  def get_global_cache_dir() -> pathlib.Path:
34
- cache_dir = get_app_path() / '.box'
38
+ cache_dir = get_global_cache_dir_path()
35
39
  cache_dir.mkdir(parents=True, exist_ok=True)
36
40
  fingerprint_file = cache_dir / 'fingerprint'
37
41
  if not fingerprint_file.is_file():