rbx.cp 0.5.42__py3-none-any.whl → 0.5.46__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/cli.py +8 -0
- rbx/box/code.py +76 -3
- rbx/box/generators.py +77 -40
- rbx/box/main.py +0 -4
- rbx/box/package.py +16 -2
- rbx/box/solutions.py +5 -3
- rbx/box/state.py +1 -0
- rbx/box/statements/builders.py +22 -3
- rbx/box/tasks.py +32 -5
- rbx/box/testcase_utils.py +66 -0
- rbx/grading/judge/cacher.py +0 -4
- rbx/grading/judge/digester.py +0 -3
- rbx/grading/judge/sandbox.py +7 -0
- rbx/grading/judge/sandboxes/isolate.py +2 -2
- rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -2
- rbx/grading/judge/sandboxes/timeit.py +109 -5
- rbx/grading/judge/storage.py +0 -4
- rbx/main.py +0 -4
- {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/METADATA +1 -2
- {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/RECORD +23 -54
- {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/WHEEL +1 -1
- rbx/testdata/box1/gen1.cpp +0 -7
- rbx/testdata/box1/gen2.cpp +0 -9
- rbx/testdata/box1/genScript.py +0 -2
- rbx/testdata/box1/hard-tle.sol.cpp +0 -26
- rbx/testdata/box1/ole.cpp +0 -17
- rbx/testdata/box1/problem.rbx.yml +0 -39
- rbx/testdata/box1/re.sol.cpp +0 -23
- rbx/testdata/box1/sol.cpp +0 -22
- rbx/testdata/box1/tests/1.in +0 -1
- rbx/testdata/box1/tle-and-incorrect.sol.cpp +0 -33
- rbx/testdata/box1/tle.sol.cpp +0 -35
- rbx/testdata/box1/validator.cpp +0 -11
- rbx/testdata/box1/wa.sol.cpp +0 -22
- rbx/testdata/caching/executable.py +0 -1
- rbx/testdata/compatible +0 -0
- rbx/testdata/interactive/checker.cpp +0 -21
- rbx/testdata/interactive/gen.cpp +0 -11
- rbx/testdata/interactive/interactor.cpp +0 -63
- rbx/testdata/interactive/problem.rbx.yml +0 -40
- rbx/testdata/interactive/sols/af_ac_pe.cpp +0 -75
- rbx/testdata/interactive/sols/af_ac_re.cpp +0 -76
- rbx/testdata/interactive/sols/af_ac_too_many_iter.cpp +0 -72
- rbx/testdata/interactive/sols/af_inf_cout_with_flush.cpp +0 -79
- rbx/testdata/interactive/sols/af_inf_cout_without_flush.cpp +0 -78
- rbx/testdata/interactive/sols/af_ml.cpp +0 -78
- rbx/testdata/interactive/sols/af_tl_after_ans.cpp +0 -74
- rbx/testdata/interactive/sols/af_wa.cpp +0 -74
- rbx/testdata/interactive/sols/interactive-binary-search_mm_naive_cin.cpp +0 -17
- rbx/testdata/interactive/sols/main.cpp +0 -26
- rbx/testdata/interactive/testplan.txt +0 -6
- rbx/testdata/interactive/validator.cpp +0 -16
- {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/entry_points.txt +0 -0
rbx/box/cli.py
CHANGED
@@ -103,6 +103,13 @@ def main(
|
|
103
103
|
help='Whether to compile and run testlib components with sanitizers enabled. '
|
104
104
|
'If you want to run the solutions with sanitizers enabled, use the "-s" flag in the corresponding run command.',
|
105
105
|
),
|
106
|
+
debug_logs: bool = typer.Option(
|
107
|
+
False,
|
108
|
+
'--debug-logs',
|
109
|
+
'--debug',
|
110
|
+
'-d',
|
111
|
+
help='Whether to save extra debug logs along with the evaluation results.',
|
112
|
+
),
|
106
113
|
):
|
107
114
|
state.STATE.run_through_cli = True
|
108
115
|
state.STATE.sanitized = sanitized
|
@@ -111,6 +118,7 @@ def main(
|
|
111
118
|
'[warning]Sanitizers are running just for testlib components.\n'
|
112
119
|
'If you want to run the solutions with sanitizers enabled, use the [item]-s[/item] flag in the corresponding run command.[/warning]'
|
113
120
|
)
|
121
|
+
state.STATE.debug_logs = debug_logs
|
114
122
|
|
115
123
|
|
116
124
|
# @app.command('ui', hidden=True)
|
rbx/box/code.py
CHANGED
@@ -216,6 +216,52 @@ class PreparedRun:
|
|
216
216
|
metadata: RunLogMetadata
|
217
217
|
|
218
218
|
|
219
|
+
@dataclasses.dataclass
|
220
|
+
class CaptureSpec:
|
221
|
+
prefix: str
|
222
|
+
output: Optional[DigestOrDest] = None
|
223
|
+
merged_capture: Optional[pathlib.Path] = None
|
224
|
+
|
225
|
+
|
226
|
+
def _prepare_for_communication(
|
227
|
+
run: PreparedRun,
|
228
|
+
stdin: pathlib.Path,
|
229
|
+
stdout: pathlib.Path,
|
230
|
+
reverse_io: bool = False,
|
231
|
+
capture: Optional[CaptureSpec] = None,
|
232
|
+
):
|
233
|
+
run.sandbox_params.set_stdio(
|
234
|
+
stdin=stdin,
|
235
|
+
stdout=stdout,
|
236
|
+
)
|
237
|
+
run.sandbox_params.reverse_io = reverse_io
|
238
|
+
if capture is not None:
|
239
|
+
run.sandbox_params.timeit_prefix = capture.prefix
|
240
|
+
|
241
|
+
if capture.output is not None:
|
242
|
+
output_path = PosixPath('capture')
|
243
|
+
run.sandbox_params.timeit_dups['do'].append(output_path)
|
244
|
+
|
245
|
+
run.artifacts.outputs.append(
|
246
|
+
GradingFileOutput(
|
247
|
+
src=output_path,
|
248
|
+
**capture.output.expand(),
|
249
|
+
touch=True,
|
250
|
+
)
|
251
|
+
)
|
252
|
+
|
253
|
+
if capture.merged_capture is not None:
|
254
|
+
merged_output_path = package.get_merged_capture_path().resolve()
|
255
|
+
run.sandbox_params.timeit_dups['Do'].append(merged_output_path)
|
256
|
+
|
257
|
+
run.artifacts.outputs.append(
|
258
|
+
GradingFileOutput(
|
259
|
+
src=merged_output_path,
|
260
|
+
dest=capture.merged_capture,
|
261
|
+
)
|
262
|
+
)
|
263
|
+
|
264
|
+
|
219
265
|
def _prepare_run(
|
220
266
|
code: CodeItem,
|
221
267
|
executable: DigestOrSource,
|
@@ -484,6 +530,7 @@ class CommunicationItem:
|
|
484
530
|
outputs: Optional[List[GradingFileOutput]] = None
|
485
531
|
extra_args: Optional[str] = None
|
486
532
|
extra_config: Optional[ExecutionConfig] = None
|
533
|
+
capture: Optional[DigestOrDest] = None
|
487
534
|
|
488
535
|
def prepare(self) -> PreparedRun:
|
489
536
|
return _prepare_run(
|
@@ -500,6 +547,7 @@ class CommunicationItem:
|
|
500
547
|
async def run_communication(
|
501
548
|
interactor: CommunicationItem,
|
502
549
|
solution: CommunicationItem,
|
550
|
+
merged_capture: Optional[pathlib.Path] = None,
|
503
551
|
retry_index: Optional[int] = None,
|
504
552
|
):
|
505
553
|
fifo_in, fifo_out = package.get_fifos()
|
@@ -510,10 +558,35 @@ async def run_communication(
|
|
510
558
|
interactor_prepared.metadata.retryIndex = retry_index
|
511
559
|
solution_prepared.metadata.retryIndex = retry_index
|
512
560
|
|
513
|
-
|
514
|
-
|
561
|
+
interactor_prefix = 'INTERACTOR:'
|
562
|
+
solution_prefix = 'SOLUTION:'
|
563
|
+
|
564
|
+
if merged_capture is not None:
|
565
|
+
package.get_merged_capture_path().write_text(
|
566
|
+
f'{interactor_prefix}\n{solution_prefix}\n'
|
567
|
+
)
|
515
568
|
|
516
|
-
|
569
|
+
_prepare_for_communication(
|
570
|
+
interactor_prepared,
|
571
|
+
fifo_out,
|
572
|
+
fifo_in,
|
573
|
+
capture=CaptureSpec(
|
574
|
+
prefix=interactor_prefix,
|
575
|
+
output=interactor.capture,
|
576
|
+
merged_capture=merged_capture,
|
577
|
+
),
|
578
|
+
)
|
579
|
+
_prepare_for_communication(
|
580
|
+
solution_prepared,
|
581
|
+
fifo_in,
|
582
|
+
fifo_out,
|
583
|
+
reverse_io=True,
|
584
|
+
capture=CaptureSpec(
|
585
|
+
prefix=solution_prefix,
|
586
|
+
output=solution.capture,
|
587
|
+
merged_capture=merged_capture,
|
588
|
+
),
|
589
|
+
)
|
517
590
|
|
518
591
|
interactor_run_params = steps.CoordinatedRunParams(
|
519
592
|
command=interactor_prepared.command,
|
rbx/box/generators.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import pathlib
|
2
2
|
import shutil
|
3
|
-
import tempfile
|
4
3
|
from typing import Dict, List, Optional, Set
|
5
4
|
|
6
5
|
import typer
|
@@ -63,6 +62,47 @@ def _copy_testcase_over(
|
|
63
62
|
)
|
64
63
|
|
65
64
|
|
65
|
+
def _copy_testcase_output_over(
|
66
|
+
src_output_path: pathlib.Path, dest_output_path: pathlib.Path, suffix: str
|
67
|
+
) -> bool:
|
68
|
+
dest_output_path.parent.mkdir(parents=True, exist_ok=True)
|
69
|
+
|
70
|
+
src_path = src_output_path.with_suffix(suffix)
|
71
|
+
if not src_path.is_file():
|
72
|
+
return False
|
73
|
+
|
74
|
+
shutil.copy(str(src_path), str(dest_output_path.with_suffix(suffix)))
|
75
|
+
return True
|
76
|
+
|
77
|
+
|
78
|
+
def _copy_testcase_outputs_over(
|
79
|
+
testcase: Testcase, dest: Testcase, pipes: bool = False
|
80
|
+
):
|
81
|
+
assert dest.outputPath is not None
|
82
|
+
dest.outputPath.parent.mkdir(parents=True, exist_ok=True)
|
83
|
+
|
84
|
+
has_copied = False
|
85
|
+
|
86
|
+
if testcase.outputPath is not None and testcase.outputPath.is_file():
|
87
|
+
shutil.copy(str(testcase.outputPath), str(dest.outputPath))
|
88
|
+
has_copied = True
|
89
|
+
|
90
|
+
if not pipes:
|
91
|
+
return has_copied
|
92
|
+
|
93
|
+
reference_path = testcase.outputPath or testcase.inputPath
|
94
|
+
if _copy_testcase_output_over(reference_path, dest.outputPath, '.pin'):
|
95
|
+
has_copied = True
|
96
|
+
|
97
|
+
if _copy_testcase_output_over(reference_path, dest.outputPath, '.pout'):
|
98
|
+
has_copied = True
|
99
|
+
|
100
|
+
if _copy_testcase_output_over(reference_path, dest.outputPath, '.pio'):
|
101
|
+
has_copied = True
|
102
|
+
|
103
|
+
return has_copied
|
104
|
+
|
105
|
+
|
66
106
|
def get_all_built_testcases() -> Dict[str, List[Testcase]]:
|
67
107
|
pkg = package.find_problem_package_or_die()
|
68
108
|
res = {group.name: find_built_testcases(group) for group in pkg.testcases}
|
@@ -256,59 +296,46 @@ async def generate_testcases(
|
|
256
296
|
async def generate_output_for_testcase(
|
257
297
|
main_solution_digest: str,
|
258
298
|
testcase: Testcase,
|
259
|
-
stderr_path: Optional[pathlib.Path] = None,
|
260
299
|
interactor_digest: Optional[str] = None,
|
261
300
|
):
|
262
301
|
assert testcase.outputPath is not None
|
263
302
|
testcase.inputPath.parent.mkdir(parents=True, exist_ok=True)
|
264
303
|
testcase.outputPath.parent.mkdir(parents=True, exist_ok=True)
|
265
304
|
|
266
|
-
if testcase.outputPath.is_file():
|
267
|
-
# Output file was already copied over from manual tests.
|
268
|
-
return
|
269
|
-
|
270
305
|
main_solution = package.get_main_solution()
|
271
306
|
if main_solution is None:
|
272
307
|
return
|
273
308
|
|
274
|
-
|
275
|
-
|
309
|
+
eval: Evaluation = await run_solution_on_testcase(
|
310
|
+
main_solution,
|
311
|
+
main_solution_digest,
|
312
|
+
None,
|
313
|
+
testcase,
|
314
|
+
interactor_digest=interactor_digest,
|
315
|
+
use_retries=False,
|
316
|
+
use_timelimit=False,
|
317
|
+
capture_pipes=True,
|
318
|
+
)
|
276
319
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
None,
|
281
|
-
testcase,
|
282
|
-
output_dir,
|
283
|
-
interactor_digest=interactor_digest,
|
284
|
-
use_retries=False,
|
285
|
-
use_timelimit=False,
|
320
|
+
if eval.result.outcome != Outcome.ACCEPTED:
|
321
|
+
console.console.print(
|
322
|
+
f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
|
286
323
|
)
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
console.console.print(f'[error]Summary:[/error] {eval.log.get_summary()}')
|
298
|
-
console.console.print(
|
299
|
-
f'[warning]Verdict: [item]{eval.result.outcome.value}[/item][/warning]',
|
300
|
-
)
|
301
|
-
console.console.print(
|
302
|
-
f'[warning]Message: [info]{eval.result.message}[/info][/warning]',
|
303
|
-
)
|
304
|
-
console.console.print(f'Input written at [item]{testcase.inputPath}[/item]')
|
324
|
+
console.console.print(f'[error]Summary:[/error] {eval.log.get_summary()}')
|
325
|
+
console.console.print(
|
326
|
+
f'[warning]Verdict: [item]{eval.result.outcome.value}[/item][/warning]',
|
327
|
+
)
|
328
|
+
console.console.print(
|
329
|
+
f'[warning]Message: [info]{eval.result.message}[/info][/warning]',
|
330
|
+
)
|
331
|
+
console.console.print(f'Input written at [item]{testcase.inputPath}[/item]')
|
332
|
+
console.console.print(f'Output written at [item]{testcase.outputPath}[/item]')
|
333
|
+
if eval.log.stderr_absolute_path is not None:
|
305
334
|
console.console.print(
|
306
|
-
f'
|
335
|
+
f'Stderr written at [item]{eval.log.stderr_absolute_path}[/item]'
|
307
336
|
)
|
308
|
-
if stderr_path is not None:
|
309
|
-
console.console.print(f'Stderr written at [item]{stderr_path}[/item]')
|
310
337
|
|
311
|
-
|
338
|
+
raise typer.Exit(1)
|
312
339
|
|
313
340
|
|
314
341
|
async def generate_outputs_for_testcases(
|
@@ -350,6 +377,14 @@ async def generate_outputs_for_testcases(
|
|
350
377
|
return
|
351
378
|
assert tc.outputPath is not None
|
352
379
|
|
380
|
+
if entry.metadata.copied_from is not None and _copy_testcase_outputs_over(
|
381
|
+
entry.metadata.copied_from, tc
|
382
|
+
):
|
383
|
+
# Copy remaining pipe files.
|
384
|
+
_copy_testcase_outputs_over(entry.metadata.copied_from, tc, pipes=True)
|
385
|
+
step()
|
386
|
+
continue
|
387
|
+
|
353
388
|
if (
|
354
389
|
main_solution is None or solution_digest is None
|
355
390
|
) and not tc.outputPath.is_file():
|
@@ -362,7 +397,9 @@ async def generate_outputs_for_testcases(
|
|
362
397
|
await generate_output_for_testcase(
|
363
398
|
solution_digest,
|
364
399
|
tc,
|
365
|
-
gen_runs_dir / 'main.stderr',
|
366
400
|
interactor_digest=interactor_digest,
|
367
401
|
)
|
402
|
+
if entry.metadata.copied_from is not None:
|
403
|
+
# Copy remaining pipe files.
|
404
|
+
_copy_testcase_outputs_over(entry.metadata.copied_from, tc, pipes=True)
|
368
405
|
step()
|
rbx/box/main.py
CHANGED
rbx/box/package.py
CHANGED
@@ -393,16 +393,23 @@ def get_compilation_files(code: CodeItem) -> List[Tuple[pathlib.Path, pathlib.Pa
|
|
393
393
|
return res
|
394
394
|
|
395
395
|
|
396
|
+
@functools.cache
|
397
|
+
def get_shared_dir(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
|
398
|
+
shared_dir = get_problem_cache_dir(root) / '.shared'
|
399
|
+
shared_dir.mkdir(parents=True, exist_ok=True)
|
400
|
+
return shared_dir
|
401
|
+
|
402
|
+
|
396
403
|
@functools.cache
|
397
404
|
def get_empty_sentinel_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
|
398
|
-
path =
|
405
|
+
path = get_shared_dir(root) / '.empty'
|
399
406
|
path.write_text('')
|
400
407
|
return path
|
401
408
|
|
402
409
|
|
403
410
|
@functools.cache
|
404
411
|
def get_fifos(root: pathlib.Path = pathlib.Path()) -> Tuple[pathlib.Path, pathlib.Path]:
|
405
|
-
path =
|
412
|
+
path = get_shared_dir(root) / '.fifos'
|
406
413
|
shutil.rmtree(path, ignore_errors=True)
|
407
414
|
path.mkdir(parents=True, exist_ok=True)
|
408
415
|
fifo_in = path / 'fifo.in'
|
@@ -412,6 +419,13 @@ def get_fifos(root: pathlib.Path = pathlib.Path()) -> Tuple[pathlib.Path, pathli
|
|
412
419
|
return fifo_in, fifo_out
|
413
420
|
|
414
421
|
|
422
|
+
@functools.cache
|
423
|
+
def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
|
424
|
+
path = get_shared_dir(root) / '.merged_capture'
|
425
|
+
path.write_text('')
|
426
|
+
return path
|
427
|
+
|
428
|
+
|
415
429
|
def clear_package_cache():
|
416
430
|
pkgs = [sys.modules[__name__]]
|
417
431
|
|
rbx/box/solutions.py
CHANGED
@@ -185,7 +185,7 @@ def _run_solution(
|
|
185
185
|
compiled_digest,
|
186
186
|
checker_digest,
|
187
187
|
testcase,
|
188
|
-
output_path,
|
188
|
+
output_dir=output_path,
|
189
189
|
interactor_digest=interactor_digest,
|
190
190
|
testcase_index=i,
|
191
191
|
verification=verification,
|
@@ -367,6 +367,7 @@ async def _generate_testcase_interactively(
|
|
367
367
|
)
|
368
368
|
|
369
369
|
is_manual = False
|
370
|
+
is_output_manual = False
|
370
371
|
generation_metadata = None
|
371
372
|
if generator is not None:
|
372
373
|
generation_metadata = GenerationMetadata(
|
@@ -398,6 +399,7 @@ async def _generate_testcase_interactively(
|
|
398
399
|
output = console.multiline_prompt('Testcase output')
|
399
400
|
testcase.outputPath.write_text(output)
|
400
401
|
console.console.print()
|
402
|
+
is_output_manual = True
|
401
403
|
|
402
404
|
generation_metadata = GenerationMetadata(
|
403
405
|
copied_to=testcase,
|
@@ -453,7 +455,7 @@ async def _generate_testcase_interactively(
|
|
453
455
|
)
|
454
456
|
raise
|
455
457
|
|
456
|
-
if main_solution_digest is not None:
|
458
|
+
if main_solution_digest is not None and not is_output_manual:
|
457
459
|
pkg = package.find_problem_package_or_die()
|
458
460
|
if pkg.type == TaskType.COMMUNICATION:
|
459
461
|
interactor_digest = checkers.compile_interactor(progress)
|
@@ -523,7 +525,7 @@ def _run_interactive_solutions(
|
|
523
525
|
compiled_solutions[solution.path],
|
524
526
|
checker_digest,
|
525
527
|
testcase,
|
526
|
-
output_dir,
|
528
|
+
output_dir=output_dir,
|
527
529
|
interactor_digest=interactor_digest,
|
528
530
|
verification=verification,
|
529
531
|
)
|
rbx/box/state.py
CHANGED
rbx/box/statements/builders.py
CHANGED
@@ -25,6 +25,7 @@ from rbx.box.statements.schema import (
|
|
25
25
|
TexToPDF,
|
26
26
|
rbxToTeX,
|
27
27
|
)
|
28
|
+
from rbx.box.testcase_utils import TestcaseInteraction, parse_interaction
|
28
29
|
|
29
30
|
|
30
31
|
@dataclasses.dataclass
|
@@ -63,13 +64,31 @@ class StatementSample(BaseModel):
|
|
63
64
|
inputPath: pathlib.Path
|
64
65
|
outputPath: pathlib.Path
|
65
66
|
hasOutput: bool = True
|
67
|
+
interaction: Optional[TestcaseInteraction] = None
|
66
68
|
|
67
69
|
@staticmethod
|
68
70
|
def from_testcase(testcase: Testcase) -> 'StatementSample':
|
71
|
+
input_path = testcase.inputPath
|
72
|
+
output_path = testcase.outputPath
|
73
|
+
|
74
|
+
pin_path = input_path.with_suffix('.pin')
|
75
|
+
pout_path = input_path.with_suffix('.pout')
|
76
|
+
pio_path = input_path.with_suffix('.pio')
|
77
|
+
|
78
|
+
if pin_path.is_file():
|
79
|
+
input_path = pin_path
|
80
|
+
if pout_path.is_file():
|
81
|
+
output_path = pout_path
|
82
|
+
|
83
|
+
interaction = None
|
84
|
+
if pio_path.is_file():
|
85
|
+
interaction = parse_interaction(pio_path)
|
86
|
+
|
69
87
|
return StatementSample(
|
70
|
-
inputPath=
|
71
|
-
outputPath=
|
72
|
-
hasOutput=
|
88
|
+
inputPath=input_path,
|
89
|
+
outputPath=output_path or utils.get_empty_sentinel_path(),
|
90
|
+
hasOutput=output_path is not None,
|
91
|
+
interaction=interaction,
|
73
92
|
)
|
74
93
|
|
75
94
|
@staticmethod
|
rbx/box/tasks.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import pathlib
|
2
2
|
from typing import Optional
|
3
3
|
|
4
|
-
from rbx.box import checkers, package
|
4
|
+
from rbx.box import checkers, package, state
|
5
5
|
from rbx.box.code import CommunicationItem, run_communication, run_item
|
6
6
|
from rbx.box.environment import EnvironmentSandbox, ExecutionConfig, VerificationLevel
|
7
7
|
from rbx.box.retries import Retrier
|
@@ -42,13 +42,14 @@ async def run_solution_on_testcase(
|
|
42
42
|
compiled_digest: str,
|
43
43
|
checker_digest: Optional[str],
|
44
44
|
testcase: Testcase,
|
45
|
-
output_dir: pathlib.Path,
|
45
|
+
output_dir: Optional[pathlib.Path] = None,
|
46
46
|
interactor_digest: Optional[str] = None,
|
47
47
|
testcase_index: int = 0,
|
48
48
|
verification: VerificationLevel = VerificationLevel.NONE,
|
49
49
|
timelimit_override: Optional[int] = None,
|
50
50
|
use_retries: bool = True,
|
51
51
|
use_timelimit: bool = True,
|
52
|
+
capture_pipes: bool = False,
|
52
53
|
) -> Evaluation:
|
53
54
|
if interactor_digest is not None:
|
54
55
|
return await _run_communication_solution_on_testcase(
|
@@ -63,6 +64,7 @@ async def run_solution_on_testcase(
|
|
63
64
|
timelimit_override=timelimit_override,
|
64
65
|
use_retries=use_retries,
|
65
66
|
use_timelimit=use_timelimit,
|
67
|
+
capture_pipes=capture_pipes,
|
66
68
|
)
|
67
69
|
|
68
70
|
async def run_fn(retry_index: int) -> Evaluation:
|
@@ -76,7 +78,11 @@ async def run_solution_on_testcase(
|
|
76
78
|
)
|
77
79
|
extra_config = _get_execution_config(limits, actual_sandbox)
|
78
80
|
|
79
|
-
|
81
|
+
if output_dir is None:
|
82
|
+
assert testcase.outputPath is not None
|
83
|
+
output_path = testcase.outputPath
|
84
|
+
else:
|
85
|
+
output_path = output_dir / testcase.inputPath.with_suffix('.out').name
|
80
86
|
error_path = output_path.with_suffix('.err')
|
81
87
|
log_path = output_path.with_suffix('.log')
|
82
88
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
@@ -149,13 +155,16 @@ async def _run_communication_solution_on_testcase(
|
|
149
155
|
interactor_digest: str,
|
150
156
|
checker_digest: Optional[str],
|
151
157
|
testcase: Testcase,
|
152
|
-
output_dir: pathlib.Path,
|
158
|
+
output_dir: Optional[pathlib.Path] = None,
|
153
159
|
testcase_index: int = 0,
|
154
160
|
verification: VerificationLevel = VerificationLevel.NONE,
|
155
161
|
timelimit_override: Optional[int] = None,
|
156
162
|
use_retries: bool = True,
|
157
163
|
use_timelimit: bool = True,
|
164
|
+
capture_pipes: bool = False,
|
158
165
|
) -> Evaluation:
|
166
|
+
capture_pipes = capture_pipes or state.STATE.debug_logs
|
167
|
+
|
159
168
|
async def run_fn(retry_index: int) -> Evaluation:
|
160
169
|
actual_sandbox = package.get_singleton_sandbox()
|
161
170
|
interactor_sandbox = package.get_singleton_interactor_sandbox()
|
@@ -180,11 +189,18 @@ async def _run_communication_solution_on_testcase(
|
|
180
189
|
)
|
181
190
|
# TODO: maybe combine wall time limits?
|
182
191
|
|
183
|
-
|
192
|
+
if output_dir is None:
|
193
|
+
assert testcase.outputPath is not None
|
194
|
+
output_path = testcase.outputPath
|
195
|
+
else:
|
196
|
+
output_path = output_dir / testcase.inputPath.with_suffix('.out').name
|
184
197
|
error_path = output_path.with_suffix('.err')
|
185
198
|
log_path = output_path.with_suffix('.log')
|
186
199
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
187
200
|
|
201
|
+
interactor_capture_path = (
|
202
|
+
output_path.with_suffix('.pin') if capture_pipes else None
|
203
|
+
)
|
188
204
|
interactor_item = CommunicationItem(
|
189
205
|
code=package.get_interactor(),
|
190
206
|
executable=DigestOrSource.create(interactor_digest),
|
@@ -204,17 +220,28 @@ async def _run_communication_solution_on_testcase(
|
|
204
220
|
touch=True,
|
205
221
|
)
|
206
222
|
],
|
223
|
+
capture=DigestOrDest.create(interactor_capture_path)
|
224
|
+
if interactor_capture_path
|
225
|
+
else None,
|
226
|
+
)
|
227
|
+
solution_capture_path = (
|
228
|
+
output_path.with_suffix('.pout') if capture_pipes else None
|
207
229
|
)
|
208
230
|
solution_item = CommunicationItem(
|
209
231
|
code=solution,
|
210
232
|
executable=DigestOrSource.create(compiled_digest),
|
211
233
|
extra_config=extra_config,
|
234
|
+
capture=DigestOrDest.create(solution_capture_path)
|
235
|
+
if solution_capture_path
|
236
|
+
else None,
|
212
237
|
)
|
213
238
|
|
239
|
+
merged_capture_path = output_path.with_suffix('.pio') if capture_pipes else None
|
214
240
|
interactor_run_log, run_log = await run_communication(
|
215
241
|
interactor=interactor_item,
|
216
242
|
solution=solution_item,
|
217
243
|
retry_index=retry_index,
|
244
|
+
merged_capture=merged_capture_path,
|
218
245
|
)
|
219
246
|
|
220
247
|
checker_result = await checkers.check_communication(
|
rbx/box/testcase_utils.py
CHANGED
@@ -97,6 +97,16 @@ class TestcaseData(BaseModel):
|
|
97
97
|
output: str
|
98
98
|
|
99
99
|
|
100
|
+
class TestcaseInteractionEntry(BaseModel):
|
101
|
+
data: str
|
102
|
+
pipe: int
|
103
|
+
|
104
|
+
|
105
|
+
class TestcaseInteraction(BaseModel):
|
106
|
+
entries: List[TestcaseInteractionEntry]
|
107
|
+
prefixes: Tuple[str, str]
|
108
|
+
|
109
|
+
|
100
110
|
def find_built_testcases(group: TestcaseGroup) -> List[Testcase]:
|
101
111
|
inputs = find_built_testcase_inputs(group)
|
102
112
|
|
@@ -143,3 +153,59 @@ def fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
|
|
143
153
|
if output_path.is_file():
|
144
154
|
res.outputPath = output_path
|
145
155
|
return res
|
156
|
+
|
157
|
+
|
158
|
+
def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
159
|
+
entries = []
|
160
|
+
with file.open('r') as f:
|
161
|
+
try:
|
162
|
+
interactor_prefix = f.readline().strip()
|
163
|
+
solution_prefix = f.readline().strip()
|
164
|
+
except Exception:
|
165
|
+
console.console.print(
|
166
|
+
f'[error]Failed to read interaction file [item]{file}[/item]. Expected the first two lines to be the interactor and solution prefixes.[/error]'
|
167
|
+
)
|
168
|
+
raise typer.Exit(1) from None
|
169
|
+
|
170
|
+
rest = f.read()
|
171
|
+
start = 0
|
172
|
+
|
173
|
+
def _find_next_prefix(start: int) -> Optional[Tuple[int, int]]:
|
174
|
+
interactor_idx = rest.find(interactor_prefix, start)
|
175
|
+
solution_idx = rest.find(solution_prefix, start)
|
176
|
+
if interactor_idx == -1 and solution_idx == -1:
|
177
|
+
return None
|
178
|
+
if interactor_idx == -1:
|
179
|
+
return (solution_idx, solution_idx + len(solution_prefix))
|
180
|
+
if solution_idx == -1:
|
181
|
+
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
182
|
+
if interactor_idx < solution_idx:
|
183
|
+
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
184
|
+
return (solution_idx, solution_idx + len(solution_prefix))
|
185
|
+
|
186
|
+
def _find_next_block() -> Optional[Tuple[int, Tuple[int, int]]]:
|
187
|
+
prefix = _find_next_prefix(start)
|
188
|
+
if prefix is None:
|
189
|
+
return None
|
190
|
+
prefix_start, prefix_end = prefix
|
191
|
+
prefix = rest[prefix_start:prefix_end]
|
192
|
+
pipe = 1 if prefix == solution_prefix else 0
|
193
|
+
|
194
|
+
nxt = _find_next_prefix(prefix_end)
|
195
|
+
if nxt is None:
|
196
|
+
return (pipe, (prefix_end, len(rest)))
|
197
|
+
nxt_start, _ = nxt
|
198
|
+
return (pipe, (prefix_end, nxt_start))
|
199
|
+
|
200
|
+
while True:
|
201
|
+
block = _find_next_block()
|
202
|
+
if block is None:
|
203
|
+
break
|
204
|
+
pipe, (st, nd) = block
|
205
|
+
entries.append(TestcaseInteractionEntry(data=rest[st:nd], pipe=pipe))
|
206
|
+
start = nd
|
207
|
+
|
208
|
+
return TestcaseInteraction(
|
209
|
+
prefixes=(interactor_prefix, solution_prefix),
|
210
|
+
entries=entries,
|
211
|
+
)
|
rbx/grading/judge/cacher.py
CHANGED
@@ -9,8 +9,6 @@ import tempfile
|
|
9
9
|
import typing
|
10
10
|
from typing import IO, List, Optional
|
11
11
|
|
12
|
-
import gevent
|
13
|
-
|
14
12
|
from rbx.grading.judge import digester, storage
|
15
13
|
|
16
14
|
logger = logging.getLogger(__name__)
|
@@ -318,8 +316,6 @@ class FileCacher:
|
|
318
316
|
d.update(buf)
|
319
317
|
while len(buf) > 0:
|
320
318
|
written = dst.write(buf)
|
321
|
-
# Cooperative yield.
|
322
|
-
gevent.sleep(0)
|
323
319
|
if written is None:
|
324
320
|
break
|
325
321
|
buf = buf[written:]
|
rbx/grading/judge/digester.py
CHANGED
@@ -2,8 +2,6 @@ import hashlib
|
|
2
2
|
import pathlib
|
3
3
|
from typing import IO
|
4
4
|
|
5
|
-
import gevent
|
6
|
-
|
7
5
|
|
8
6
|
class Digester:
|
9
7
|
"""Simple wrapper of hashlib using our preferred hasher."""
|
@@ -26,7 +24,6 @@ def digest_cooperatively_into_digester(
|
|
26
24
|
buf = f.read(chunk_size)
|
27
25
|
while len(buf) > 0:
|
28
26
|
digester.update(buf)
|
29
|
-
gevent.sleep(0)
|
30
27
|
buf = f.read(chunk_size)
|
31
28
|
|
32
29
|
|
rbx/grading/judge/sandbox.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import abc
|
2
|
+
import collections
|
2
3
|
import dataclasses
|
3
4
|
import io
|
4
5
|
import logging
|
@@ -114,6 +115,12 @@ class SandboxParams(pydantic.BaseModel):
|
|
114
115
|
extra_timeout: Optional[int] = None # ms
|
115
116
|
reverse_io: bool = False
|
116
117
|
|
118
|
+
# For timeit
|
119
|
+
timeit_dups: Dict[str, List[pathlib.Path]] = dataclasses.field(
|
120
|
+
default_factory=lambda: collections.defaultdict(list)
|
121
|
+
)
|
122
|
+
timeit_prefix: Optional[str] = None
|
123
|
+
|
117
124
|
def get_cacheable_params(self) -> Dict[str, Any]:
|
118
125
|
return self.model_dump(mode='json', exclude_unset=True, exclude_none=True)
|
119
126
|
|