rbx.cp 0.5.39__py3-none-any.whl → 0.5.42__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 (53) hide show
  1. rbx/box/builder.py +6 -6
  2. rbx/box/checkers.py +105 -26
  3. rbx/box/cli.py +860 -0
  4. rbx/box/code.py +199 -84
  5. rbx/box/contest/statements.py +4 -2
  6. rbx/box/generators.py +55 -49
  7. rbx/box/generators_test.py +7 -7
  8. rbx/box/main.py +1 -852
  9. rbx/box/package.py +42 -1
  10. rbx/box/packaging/boca/packager.py +2 -1
  11. rbx/box/packaging/main.py +24 -7
  12. rbx/box/packaging/moj/packager.py +164 -0
  13. rbx/box/retries.py +5 -5
  14. rbx/box/schema.py +86 -4
  15. rbx/box/solutions.py +46 -108
  16. rbx/box/solutions_test.py +5 -6
  17. rbx/box/statements/build_statements.py +4 -2
  18. rbx/box/stresses.py +23 -12
  19. rbx/box/tasks.py +258 -0
  20. rbx/box/testcase_extractors.py +21 -21
  21. rbx/box/testcases/main.py +19 -14
  22. rbx/box/unit.py +116 -0
  23. rbx/box/validators.py +27 -18
  24. rbx/box/validators_test.py +3 -3
  25. rbx/grading/judge/sandbox.py +8 -0
  26. rbx/grading/judge/sandboxes/stupid_sandbox.py +12 -7
  27. rbx/grading/judge/sandboxes/timeit.py +8 -2
  28. rbx/grading/steps.py +76 -2
  29. rbx/grading/steps_with_caching.py +45 -3
  30. rbx/grading/steps_with_caching_run_test.py +51 -49
  31. rbx/resources/packagers/moj/scripts/compare.sh +101 -0
  32. rbx/test.py +6 -4
  33. rbx/testdata/interactive/checker.cpp +21 -0
  34. rbx/testdata/interactive/gen.cpp +11 -0
  35. rbx/testdata/interactive/interactor.cpp +63 -0
  36. rbx/testdata/interactive/problem.rbx.yml +40 -0
  37. rbx/testdata/interactive/sols/af_ac_pe.cpp +75 -0
  38. rbx/testdata/interactive/sols/af_ac_re.cpp +76 -0
  39. rbx/testdata/interactive/sols/af_ac_too_many_iter.cpp +72 -0
  40. rbx/testdata/interactive/sols/af_inf_cout_with_flush.cpp +79 -0
  41. rbx/testdata/interactive/sols/af_inf_cout_without_flush.cpp +78 -0
  42. rbx/testdata/interactive/sols/af_ml.cpp +78 -0
  43. rbx/testdata/interactive/sols/af_tl_after_ans.cpp +74 -0
  44. rbx/testdata/interactive/sols/af_wa.cpp +74 -0
  45. rbx/testdata/interactive/sols/interactive-binary-search_mm_naive_cin.cpp +17 -0
  46. rbx/testdata/interactive/sols/main.cpp +26 -0
  47. rbx/testdata/interactive/testplan.txt +6 -0
  48. rbx/testdata/interactive/validator.cpp +16 -0
  49. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/METADATA +2 -1
  50. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/RECORD +53 -32
  51. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/LICENSE +0 -0
  52. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/WHEEL +0 -0
  53. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/entry_points.txt +0 -0
@@ -30,7 +30,7 @@ def _get_group_output(
30
30
  return group_path / f'{subgroup_prefix}{i:03d}.out'
31
31
 
32
32
 
33
- def _run_generator_script(testcase: TestcaseSubgroup) -> str:
33
+ async def _run_generator_script(testcase: TestcaseSubgroup) -> str:
34
34
  assert testcase.generatorScript is not None
35
35
 
36
36
  cacher = package.get_file_cacher()
@@ -54,7 +54,7 @@ def _run_generator_script(testcase: TestcaseSubgroup) -> str:
54
54
  raise
55
55
 
56
56
  run_stderr = DigestHolder()
57
- run_log = run_item(
57
+ run_log = await run_item(
58
58
  testcase.generatorScript,
59
59
  DigestOrSource.create(compiled_digest),
60
60
  stdout=DigestOrDest.create(script_digest),
@@ -116,7 +116,7 @@ class GenerationTestcaseEntry(BaseModel):
116
116
 
117
117
  class TestcaseVisitor(abc.ABC):
118
118
  @abc.abstractmethod
119
- def visit(self, entry: GenerationTestcaseEntry):
119
+ async def visit(self, entry: GenerationTestcaseEntry):
120
120
  pass
121
121
 
122
122
  def should_visit_group(self, group_name: str) -> bool:
@@ -139,10 +139,10 @@ class TestcaseGroupVisitor(TestcaseVisitor):
139
139
  return self.groups is None or group_name in self.groups
140
140
 
141
141
 
142
- def run_testcase_visitor(visitor: TestcaseVisitor):
142
+ async def run_testcase_visitor(visitor: TestcaseVisitor):
143
143
  pkg = package.find_problem_package_or_die()
144
144
 
145
- def _explore_subgroup(
145
+ async def _explore_subgroup(
146
146
  subgroup: TestcaseSubgroup,
147
147
  subgroup_index: Optional[int],
148
148
  prefix: List[str],
@@ -179,7 +179,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
179
179
  i = 0
180
180
  # Individual testcases.
181
181
  for tc in subgroup.testcases or []:
182
- visitor.visit(
182
+ await visitor.visit(
183
183
  GenerationTestcaseEntry(
184
184
  group_entry=_entry(i),
185
185
  subgroup_entry=_sub_entry(i),
@@ -202,7 +202,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
202
202
  continue
203
203
 
204
204
  tc = Testcase(inputPath=input_path)
205
- visitor.visit(
205
+ await visitor.visit(
206
206
  GenerationTestcaseEntry(
207
207
  group_entry=_entry(i),
208
208
  subgroup_entry=_sub_entry(i),
@@ -218,7 +218,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
218
218
 
219
219
  # Single generators.
220
220
  for generator_call in subgroup.generators:
221
- visitor.visit(
221
+ await visitor.visit(
222
222
  GenerationTestcaseEntry(
223
223
  group_entry=_entry(i),
224
224
  subgroup_entry=_sub_entry(i),
@@ -237,12 +237,12 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
237
237
 
238
238
  # Run generator script.
239
239
  if subgroup.generatorScript is not None:
240
- script = _run_generator_script(subgroup)
240
+ script = await _run_generator_script(subgroup)
241
241
 
242
242
  # Run each line from generator script.
243
243
  for generator_name, args, line_number in _extract_script_lines(script):
244
244
  call = GeneratorCall(name=generator_name, args=args)
245
- visitor.visit(
245
+ await visitor.visit(
246
246
  GenerationTestcaseEntry(
247
247
  group_entry=_entry(i),
248
248
  subgroup_entry=_sub_entry(i),
@@ -269,7 +269,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
269
269
  group_validator = group.validator
270
270
 
271
271
  extra_validators = group.extraValidators
272
- _explore_subgroup(
272
+ await _explore_subgroup(
273
273
  group,
274
274
  0 if group.subgroups else None,
275
275
  [group.name],
@@ -278,7 +278,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
278
278
  )
279
279
 
280
280
  for i, subgroup in enumerate(group.subgroups):
281
- _explore_subgroup(
281
+ await _explore_subgroup(
282
282
  subgroup,
283
283
  i + 1,
284
284
  [group.name, subgroup.name],
@@ -287,7 +287,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
287
287
  )
288
288
 
289
289
 
290
- def extract_generation_testcases(
290
+ async def extract_generation_testcases(
291
291
  entries: List[TestcaseEntry],
292
292
  ) -> List[GenerationTestcaseEntry]:
293
293
  # TODO: support subgroups.
@@ -300,30 +300,30 @@ def extract_generation_testcases(
300
300
  def should_visit_group(self, group_name: str) -> bool:
301
301
  return group_name in groups
302
302
 
303
- def visit(self, entry: GenerationTestcaseEntry):
303
+ async def visit(self, entry: GenerationTestcaseEntry):
304
304
  # TODO: support subgroups.
305
305
  if entry.group_entry.key() not in entry_keys:
306
306
  return
307
307
  res.append(entry)
308
308
 
309
- run_testcase_visitor(ExtractGenerationTestcasesVisitor())
309
+ await run_testcase_visitor(ExtractGenerationTestcasesVisitor())
310
310
  return res
311
311
 
312
312
 
313
- def extract_generation_testcases_from_groups(
313
+ async def extract_generation_testcases_from_groups(
314
314
  groups: Optional[Set[str]] = None,
315
315
  ) -> List[GenerationTestcaseEntry]:
316
316
  res: List[GenerationTestcaseEntry] = []
317
317
 
318
318
  class ExtractGenerationTestcasesVisitor(TestcaseGroupVisitor):
319
- def visit(self, entry: GenerationTestcaseEntry):
319
+ async def visit(self, entry: GenerationTestcaseEntry):
320
320
  res.append(entry)
321
321
 
322
- run_testcase_visitor(ExtractGenerationTestcasesVisitor(groups))
322
+ await run_testcase_visitor(ExtractGenerationTestcasesVisitor(groups))
323
323
  return res
324
324
 
325
325
 
326
- def extract_generation_testcases_from_patterns(
326
+ async def extract_generation_testcases_from_patterns(
327
327
  patterns: List[TestcasePattern],
328
328
  ) -> List[GenerationTestcaseEntry]:
329
329
  res: List[GenerationTestcaseEntry] = []
@@ -337,12 +337,12 @@ def extract_generation_testcases_from_patterns(
337
337
  pattern.intersecting_group(subgroup_path) for pattern in patterns
338
338
  )
339
339
 
340
- def visit(self, entry: GenerationTestcaseEntry):
340
+ async def visit(self, entry: GenerationTestcaseEntry):
341
341
  if not any(
342
342
  pattern.match(entry.group_entry) for pattern in patterns
343
343
  ) and not any(pattern.match(entry.subgroup_entry) for pattern in patterns):
344
344
  return
345
345
  res.append(entry)
346
346
 
347
- run_testcase_visitor(ExtractGenerationTestcasesVisitor())
347
+ await run_testcase_visitor(ExtractGenerationTestcasesVisitor())
348
348
  return res
rbx/box/testcases/main.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import pathlib
2
2
  from typing import Annotated, List, Optional
3
3
 
4
+ import syncer
4
5
  import typer
5
6
 
6
7
  from rbx import annotations, config, utils
@@ -20,8 +21,8 @@ from rbx.console import console
20
21
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
21
22
 
22
23
 
23
- def _find_testcase(entry: TestcaseEntry) -> GenerationTestcaseEntry:
24
- extracted = extract_generation_testcases([entry])
24
+ async def _find_testcase(entry: TestcaseEntry) -> GenerationTestcaseEntry:
25
+ extracted = await extract_generation_testcases([entry])
25
26
  if not extracted:
26
27
  console.print(f'[error]Testcase [item]{entry}[/item] not found.[/error]')
27
28
  raise typer.Exit(1)
@@ -35,7 +36,7 @@ def _should_generate_output(entry: GenerationTestcaseEntry) -> bool:
35
36
  ) and package.get_main_solution() is not None
36
37
 
37
38
 
38
- def _generate_input_for_editing(
39
+ async def _generate_input_for_editing(
39
40
  entry: GenerationTestcaseEntry,
40
41
  output: bool = True,
41
42
  progress: Optional[utils.StatusProgress] = None,
@@ -43,7 +44,7 @@ def _generate_input_for_editing(
43
44
  if (
44
45
  output and _should_generate_output(entry)
45
46
  ) or entry.metadata.copied_from is None:
46
- generate_standalone(
47
+ await generate_standalone(
47
48
  entry.metadata,
48
49
  validate=False,
49
50
  group_entry=entry.group_entry,
@@ -54,7 +55,7 @@ def _generate_input_for_editing(
54
55
  return entry.metadata.copied_to.inputPath
55
56
 
56
57
 
57
- def _generate_output_for_editing(
58
+ async def _generate_output_for_editing(
58
59
  entry: GenerationTestcaseEntry,
59
60
  progress: Optional[utils.StatusProgress] = None,
60
61
  ) -> Optional[pathlib.Path]:
@@ -65,29 +66,32 @@ def _generate_output_for_editing(
65
66
  return entry.metadata.copied_from.outputPath
66
67
  if not _should_generate_output(entry):
67
68
  return None
68
- generate_outputs_for_testcases([entry.group_entry], progress=progress)
69
+ await generate_outputs_for_testcases([entry.group_entry], progress=progress)
69
70
  return entry.metadata.copied_to.outputPath
70
71
 
71
72
 
72
- def _generate_for_editing(
73
+ async def _generate_for_editing(
73
74
  entry: GenerationTestcaseEntry,
74
75
  input: bool,
75
76
  output: bool,
76
77
  progress: Optional[utils.StatusProgress] = None,
77
78
  ) -> List[pathlib.Path]:
78
79
  res = []
79
- input_path = _generate_input_for_editing(entry, output=output, progress=progress)
80
+ input_path = await _generate_input_for_editing(
81
+ entry, output=output, progress=progress
82
+ )
80
83
  if input:
81
84
  res.append(input_path)
82
85
  if output:
83
- output_path = _generate_output_for_editing(entry, progress=progress)
86
+ output_path = await _generate_output_for_editing(entry, progress=progress)
84
87
  if output_path is not None:
85
88
  res.append(output_path)
86
89
  return res
87
90
 
88
91
 
89
92
  @app.command('view, v', help='View a testcase in your default editor.')
90
- def view(
93
+ @syncer.sync
94
+ async def view(
91
95
  tc: Annotated[
92
96
  str,
93
97
  typer.Argument(help='Testcase to view. Format: [group]/[index].'),
@@ -112,17 +116,18 @@ def view(
112
116
  raise typer.Exit(1)
113
117
 
114
118
  entry = TestcaseEntry.parse(tc)
115
- testcase = _find_testcase(entry)
119
+ testcase = await _find_testcase(entry)
116
120
 
117
121
  with utils.StatusProgress('Preparing testcase...') as s:
118
- items = _generate_for_editing(
122
+ items = await _generate_for_editing(
119
123
  testcase, input=not output_only, output=not input_only, progress=s
120
124
  )
121
125
  config.edit_multiple(items, readonly=True)
122
126
 
123
127
 
124
128
  @app.command('info, i', help='Show information about testcases.')
125
- def info(
129
+ @syncer.sync
130
+ async def info(
126
131
  pattern: Annotated[
127
132
  Optional[str],
128
133
  typer.Argument(
@@ -131,7 +136,7 @@ def info(
131
136
  ] = None,
132
137
  ):
133
138
  tc_pattern = TestcasePattern.parse(pattern or '*')
134
- testcases = extract_generation_testcases_from_patterns([tc_pattern])
139
+ testcases = await extract_generation_testcases_from_patterns([tc_pattern])
135
140
  if not testcases:
136
141
  console.print(
137
142
  f'[error]No testcases found matching pattern [item]{pattern}[/item].[/error]'
rbx/box/unit.py ADDED
@@ -0,0 +1,116 @@
1
+ from typing import List, Optional
2
+
3
+ import syncer
4
+
5
+ from rbx import console
6
+ from rbx.box import checkers, package, validators
7
+ from rbx.box.schema import CodeItem, Testcase, ValidatorOutcome, ValidatorTest
8
+ from rbx.utils import StatusProgress
9
+
10
+
11
+ def _get_validator_for_test(test: ValidatorTest) -> Optional[CodeItem]:
12
+ pkg = package.find_problem_package_or_die()
13
+ if test.validator is not None:
14
+ return test.validator
15
+ return pkg.validator
16
+
17
+
18
+ async def run_validator_unit_tests(progress: StatusProgress):
19
+ pkg = package.find_problem_package_or_die()
20
+
21
+ vals: List[CodeItem] = []
22
+ for test in pkg.unitTests.validator:
23
+ val = _get_validator_for_test(test)
24
+ if val is not None:
25
+ vals.append(val)
26
+
27
+ compiled_validators = validators.compile_validators(vals, progress=progress)
28
+
29
+ if progress:
30
+ progress.update('Running validator unit tests...')
31
+
32
+ console.console.rule('Validator tests', style='info')
33
+
34
+ for i, test in enumerate(pkg.unitTests.validator):
35
+ val = _get_validator_for_test(test)
36
+ if val is None:
37
+ console.console.print(
38
+ f'[warning]No validator found for test [item]#{i + 1}[/item], skipping.[/warning]'
39
+ )
40
+ continue
41
+
42
+ compiled_digest = compiled_validators[str(val.path)]
43
+ info = await validators.validate_one_off(
44
+ test.input,
45
+ val,
46
+ compiled_digest,
47
+ )
48
+
49
+ is_valid = test.outcome == ValidatorOutcome.VALID
50
+
51
+ markup = (
52
+ '[success]OK[/success]' if info.ok == is_valid else '[error]FAIL[/error]'
53
+ )
54
+
55
+ console.console.print(
56
+ f'{markup} Unit test [item]#{i + 1}[/item] for [item]{test.input}[/item]'
57
+ )
58
+ console.console.print(f' [status]Expected[/status] {test.outcome.value}')
59
+ if info.ok != is_valid:
60
+ if info.ok:
61
+ console.console.print(' [status]Actual[/status] VALID')
62
+ else:
63
+ console.console.print(f' [status]Actual[/status] {info.message}')
64
+
65
+
66
+ async def run_checker_unit_tests(progress: StatusProgress):
67
+ pkg = package.find_problem_package_or_die()
68
+ if not pkg.unitTests.checker:
69
+ return
70
+
71
+ if not package.get_checker():
72
+ console.console.print(
73
+ '[warning]No checker found, skipping checker unit tests.[/warning]'
74
+ )
75
+ return
76
+
77
+ compiled_digest = checkers.compile_checker(progress=progress)
78
+
79
+ if progress:
80
+ progress.update('Running checker unit tests...')
81
+
82
+ console.console.rule('Checker tests', style='info')
83
+
84
+ empty_file = package.get_empty_sentinel_path()
85
+
86
+ for i, test in enumerate(pkg.unitTests.checker):
87
+ result = await checkers.check(
88
+ compiled_digest,
89
+ run_log=None,
90
+ testcase=Testcase(
91
+ inputPath=test.input or empty_file,
92
+ outputPath=test.answer or empty_file,
93
+ ),
94
+ program_output=test.output or empty_file,
95
+ skip_run_log=True,
96
+ )
97
+
98
+ markup = (
99
+ '[success]OK[/success]'
100
+ if test.outcome.match(result.outcome)
101
+ else '[error]FAIL[/error]'
102
+ )
103
+
104
+ console.console.print(f'{markup} Unit test [item]#{i + 1}[/item]')
105
+ console.console.print(f' [status]Expected[/status] {test.outcome.name}')
106
+
107
+ if not test.outcome.match(result.outcome):
108
+ console.console.print(f' [status]Actual[/status] {result.outcome.name}')
109
+ if result.message:
110
+ console.console.print(f' [status]Message[/status] {result.message}')
111
+
112
+
113
+ @syncer.sync
114
+ async def run_unit_tests(progress: StatusProgress):
115
+ await run_validator_unit_tests(progress)
116
+ await run_checker_unit_tests(progress)
rbx/box/validators.py CHANGED
@@ -86,7 +86,7 @@ def _has_group_specific_validator() -> bool:
86
86
  return any(group.validator is not None for group in pkg.testcases)
87
87
 
88
88
 
89
- def _validate_testcase(
89
+ async def _validate_testcase(
90
90
  testcase: pathlib.Path,
91
91
  validator: CodeItem,
92
92
  validator_digest: str,
@@ -103,7 +103,7 @@ def _validate_testcase(
103
103
 
104
104
  message_digest = DigestHolder()
105
105
  log_digest = DigestHolder()
106
- run_log = run_item(
106
+ run_log = await run_item(
107
107
  validator,
108
108
  DigestOrSource.create(validator_digest),
109
109
  stdin=DigestOrSource.create(testcase),
@@ -140,13 +140,13 @@ def _validate_testcase(
140
140
  )
141
141
 
142
142
 
143
- def _validate_test(
143
+ async def _validate_test(
144
144
  testcase: pathlib.Path,
145
145
  validator: CodeItem,
146
146
  validator_digest: str,
147
147
  ) -> Tuple[bool, Optional[str], HitBounds]:
148
148
  pkg = package.find_problem_package_or_die()
149
- return _validate_testcase(
149
+ return await _validate_testcase(
150
150
  testcase, validator, validator_digest, vars=pkg.expanded_vars
151
151
  )
152
152
 
@@ -159,12 +159,12 @@ def compile_main_validator() -> Optional[Tuple[CodeItem, str]]:
159
159
  return pkg.validator, _compile_validator(pkg.validator)
160
160
 
161
161
 
162
- def validate_one_off(
162
+ async def validate_one_off(
163
163
  testcase: pathlib.Path,
164
164
  validator: CodeItem,
165
165
  validator_digest: str,
166
166
  ) -> TestcaseValidationInfo:
167
- ok, message, _ = _validate_test(testcase, validator, validator_digest)
167
+ ok, message, _ = await _validate_test(testcase, validator, validator_digest)
168
168
  info = TestcaseValidationInfo(
169
169
  validator=validator,
170
170
  group='interactive',
@@ -177,15 +177,10 @@ def validate_one_off(
177
177
 
178
178
 
179
179
  def compile_validators(
180
- validation_entries: List[GenerationTestcaseEntry],
180
+ validators: List[CodeItem],
181
181
  progress: Optional[StatusProgress] = None,
182
182
  ) -> Dict[str, str]:
183
- validators = []
184
-
185
- for entry in validation_entries:
186
- if entry.validator is not None:
187
- validators.append(entry.validator)
188
- validators.extend(entry.extra_validators)
183
+ validator_to_compiled_digest = {}
189
184
 
190
185
  validator_to_compiled_digest = {}
191
186
 
@@ -202,7 +197,21 @@ def compile_validators(
202
197
  return validator_to_compiled_digest
203
198
 
204
199
 
205
- def validate_testcases(
200
+ def compile_validators_for_entries(
201
+ validation_entries: List[GenerationTestcaseEntry],
202
+ progress: Optional[StatusProgress] = None,
203
+ ) -> Dict[str, str]:
204
+ validators = []
205
+
206
+ for entry in validation_entries:
207
+ if entry.validator is not None:
208
+ validators.append(entry.validator)
209
+ validators.extend(entry.extra_validators)
210
+
211
+ return compile_validators(validators, progress=progress)
212
+
213
+
214
+ async def validate_testcases(
206
215
  progress: Optional[StatusProgress] = None,
207
216
  groups: Optional[Set[str]] = None,
208
217
  ) -> List[TestcaseValidationInfo]:
@@ -210,8 +219,8 @@ def validate_testcases(
210
219
  if progress is not None:
211
220
  progress.step()
212
221
 
213
- validation_entries = extract_generation_testcases_from_groups(groups)
214
- validator_to_compiled_digest = compile_validators(
222
+ validation_entries = await extract_generation_testcases_from_groups(groups)
223
+ validator_to_compiled_digest = compile_validators_for_entries(
215
224
  validation_entries, progress=progress
216
225
  )
217
226
 
@@ -225,7 +234,7 @@ def validate_testcases(
225
234
  # Main validation.
226
235
  if entry.validator is not None:
227
236
  compiled_digest = validator_to_compiled_digest[str(entry.validator.path)]
228
- ok, message, hit_bounds = _validate_test(
237
+ ok, message, hit_bounds = await _validate_test(
229
238
  input_path, entry.validator, compiled_digest
230
239
  )
231
240
  validation_info.append(
@@ -241,7 +250,7 @@ def validate_testcases(
241
250
 
242
251
  for extra_validator in entry.extra_validators:
243
252
  compiled_digest = validator_to_compiled_digest[str(extra_validator.path)]
244
- ok, message, hit_bounds = _validate_test(
253
+ ok, message, hit_bounds = await _validate_test(
245
254
  input_path, extra_validator, compiled_digest
246
255
  )
247
256
  validation_info.append(
@@ -7,9 +7,9 @@ from rbx.box.validators import validate_testcases
7
7
 
8
8
 
9
9
  @pytest.mark.test_pkg('box1')
10
- def test_validators(pkg_from_testdata: pathlib.Path):
11
- generate_testcases()
12
- validation_infos = validate_testcases()
10
+ async def test_validators(pkg_from_testdata: pathlib.Path):
11
+ await generate_testcases()
12
+ validation_infos = await validate_testcases()
13
13
 
14
14
  for info in validation_infos:
15
15
  assert info.ok
@@ -112,6 +112,7 @@ class SandboxParams(pydantic.BaseModel):
112
112
  timeout: Optional[int] = None # ms
113
113
  wallclock_timeout: Optional[int] = None # ms
114
114
  extra_timeout: Optional[int] = None # ms
115
+ reverse_io: bool = False
115
116
 
116
117
  def get_cacheable_params(self) -> Dict[str, Any]:
117
118
  return self.model_dump(mode='json', exclude_unset=True, exclude_none=True)
@@ -393,6 +394,13 @@ class SandboxBase(abc.ABC):
393
394
  return None
394
395
  return real_path
395
396
 
397
+ def create_fifo(self, path: pathlib.Path, override: bool = False):
398
+ real_path = self.relative_path(path)
399
+ if override:
400
+ real_path.unlink(missing_ok=True)
401
+ os.mkfifo(str(real_path))
402
+ return real_path
403
+
396
404
  def create_file_from_storage(
397
405
  self,
398
406
  path: pathlib.Path,
@@ -91,17 +91,22 @@ class StupidSandbox(SandboxBase):
91
91
  args.append(f'-w{walltimeout_in_s:.3f}')
92
92
  if self.params.address_space:
93
93
  args.append(f'-m{self.params.address_space}')
94
- if self.params.stdin_file:
95
- args.append(f'-i{self.params.stdin_file}')
96
- if self.params.stdout_file:
97
- args.append(f'-o{self.params.stdout_file}')
98
- if self.params.stderr_file:
99
- args.append(f'-e{self.params.stderr_file}')
100
94
  if self.params.fsize:
101
95
  args.append(f'-f{self.params.fsize}')
102
96
  if self.chdir:
103
97
  args.append(f'-c{self.chdir}')
104
- return args
98
+
99
+ file_args = []
100
+ if self.params.stdin_file:
101
+ file_args.append(f'-i{self.params.stdin_file}')
102
+ if self.params.stdout_file:
103
+ file_args.append(f'-o{self.params.stdout_file}')
104
+ if self.params.stderr_file:
105
+ file_args.append(f'-e{self.params.stderr_file}')
106
+ if self.params.reverse_io:
107
+ file_args.reverse()
108
+
109
+ return args + file_args
105
110
 
106
111
  def get_root_path(self) -> pathlib.Path:
107
112
  """Return the toplevel path of the sandbox.
@@ -9,7 +9,7 @@ from time import monotonic
9
9
  from typing import List, Optional
10
10
 
11
11
 
12
- @dataclasses.dataclass()
12
+ @dataclasses.dataclass
13
13
  class Options:
14
14
  output_file: str
15
15
  argv: List[str]
@@ -21,6 +21,7 @@ class Options:
21
21
  wall_time_limit: Optional[float] = None # seconds
22
22
  memory_limit: Optional[int] = None # kb, but passed in args as mb
23
23
  fs_limit: Optional[int] = None # kb
24
+ files_to_open: List[int] = dataclasses.field(default_factory=list)
24
25
 
25
26
 
26
27
  def exit_with(code: int):
@@ -29,6 +30,7 @@ def exit_with(code: int):
29
30
 
30
31
  def parse_opts() -> Options:
31
32
  options = Options(output_file=sys.argv[1], argv=[])
33
+ options.files_to_open = []
32
34
  num_opts = 0
33
35
  while num_opts + 2 < len(sys.argv) and sys.argv[num_opts + 2].startswith('-'):
34
36
  # Process option
@@ -41,10 +43,13 @@ def parse_opts() -> Options:
41
43
  options.memory_limit = int(opt[2:]) * 1024
42
44
  elif opt.startswith('-i'):
43
45
  options.stdin_file = opt[2:]
46
+ options.files_to_open.append(0)
44
47
  elif opt.startswith('-o'):
45
48
  options.stdout_file = opt[2:]
49
+ options.files_to_open.append(1)
46
50
  elif opt.startswith('-e'):
47
51
  options.stderr_file = opt[2:]
52
+ options.files_to_open.append(2)
48
53
  elif opt.startswith('-c'):
49
54
  options.chdir = opt[2:]
50
55
  elif opt.startswith('-f'):
@@ -92,7 +97,8 @@ def set_rlimits(options: Options):
92
97
  def redirect_fds(options: Options):
93
98
  files = [options.stdin_file, options.stdout_file, options.stderr_file]
94
99
 
95
- for i, file in enumerate(files):
100
+ for i in options.files_to_open:
101
+ file = files[i]
96
102
  if file is None:
97
103
  continue
98
104
  open_args = [