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.
- rbx/box/builder.py +6 -6
- rbx/box/checkers.py +105 -26
- rbx/box/cli.py +860 -0
- rbx/box/code.py +199 -84
- rbx/box/contest/statements.py +4 -2
- rbx/box/generators.py +55 -49
- rbx/box/generators_test.py +7 -7
- rbx/box/main.py +1 -852
- rbx/box/package.py +42 -1
- rbx/box/packaging/boca/packager.py +2 -1
- rbx/box/packaging/main.py +24 -7
- rbx/box/packaging/moj/packager.py +164 -0
- rbx/box/retries.py +5 -5
- rbx/box/schema.py +86 -4
- rbx/box/solutions.py +46 -108
- rbx/box/solutions_test.py +5 -6
- rbx/box/statements/build_statements.py +4 -2
- rbx/box/stresses.py +23 -12
- rbx/box/tasks.py +258 -0
- rbx/box/testcase_extractors.py +21 -21
- rbx/box/testcases/main.py +19 -14
- rbx/box/unit.py +116 -0
- rbx/box/validators.py +27 -18
- rbx/box/validators_test.py +3 -3
- rbx/grading/judge/sandbox.py +8 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +12 -7
- rbx/grading/judge/sandboxes/timeit.py +8 -2
- rbx/grading/steps.py +76 -2
- rbx/grading/steps_with_caching.py +45 -3
- rbx/grading/steps_with_caching_run_test.py +51 -49
- rbx/resources/packagers/moj/scripts/compare.sh +101 -0
- rbx/test.py +6 -4
- rbx/testdata/interactive/checker.cpp +21 -0
- rbx/testdata/interactive/gen.cpp +11 -0
- rbx/testdata/interactive/interactor.cpp +63 -0
- rbx/testdata/interactive/problem.rbx.yml +40 -0
- rbx/testdata/interactive/sols/af_ac_pe.cpp +75 -0
- rbx/testdata/interactive/sols/af_ac_re.cpp +76 -0
- rbx/testdata/interactive/sols/af_ac_too_many_iter.cpp +72 -0
- rbx/testdata/interactive/sols/af_inf_cout_with_flush.cpp +79 -0
- rbx/testdata/interactive/sols/af_inf_cout_without_flush.cpp +78 -0
- rbx/testdata/interactive/sols/af_ml.cpp +78 -0
- rbx/testdata/interactive/sols/af_tl_after_ans.cpp +74 -0
- rbx/testdata/interactive/sols/af_wa.cpp +74 -0
- rbx/testdata/interactive/sols/interactive-binary-search_mm_naive_cin.cpp +17 -0
- rbx/testdata/interactive/sols/main.cpp +26 -0
- rbx/testdata/interactive/testplan.txt +6 -0
- rbx/testdata/interactive/validator.cpp +16 -0
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/METADATA +2 -1
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/RECORD +53 -32
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/entry_points.txt +0 -0
rbx/box/testcase_extractors.py
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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
|
-
|
180
|
+
validators: List[CodeItem],
|
181
181
|
progress: Optional[StatusProgress] = None,
|
182
182
|
) -> Dict[str, str]:
|
183
|
-
|
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
|
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 =
|
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(
|
rbx/box/validators_test.py
CHANGED
@@ -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
|
rbx/grading/judge/sandbox.py
CHANGED
@@ -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
|
-
|
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
|
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 = [
|