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.
Files changed (54) hide show
  1. rbx/box/cli.py +8 -0
  2. rbx/box/code.py +76 -3
  3. rbx/box/generators.py +77 -40
  4. rbx/box/main.py +0 -4
  5. rbx/box/package.py +16 -2
  6. rbx/box/solutions.py +5 -3
  7. rbx/box/state.py +1 -0
  8. rbx/box/statements/builders.py +22 -3
  9. rbx/box/tasks.py +32 -5
  10. rbx/box/testcase_utils.py +66 -0
  11. rbx/grading/judge/cacher.py +0 -4
  12. rbx/grading/judge/digester.py +0 -3
  13. rbx/grading/judge/sandbox.py +7 -0
  14. rbx/grading/judge/sandboxes/isolate.py +2 -2
  15. rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -2
  16. rbx/grading/judge/sandboxes/timeit.py +109 -5
  17. rbx/grading/judge/storage.py +0 -4
  18. rbx/main.py +0 -4
  19. {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/METADATA +1 -2
  20. {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/RECORD +23 -54
  21. {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/WHEEL +1 -1
  22. rbx/testdata/box1/gen1.cpp +0 -7
  23. rbx/testdata/box1/gen2.cpp +0 -9
  24. rbx/testdata/box1/genScript.py +0 -2
  25. rbx/testdata/box1/hard-tle.sol.cpp +0 -26
  26. rbx/testdata/box1/ole.cpp +0 -17
  27. rbx/testdata/box1/problem.rbx.yml +0 -39
  28. rbx/testdata/box1/re.sol.cpp +0 -23
  29. rbx/testdata/box1/sol.cpp +0 -22
  30. rbx/testdata/box1/tests/1.in +0 -1
  31. rbx/testdata/box1/tle-and-incorrect.sol.cpp +0 -33
  32. rbx/testdata/box1/tle.sol.cpp +0 -35
  33. rbx/testdata/box1/validator.cpp +0 -11
  34. rbx/testdata/box1/wa.sol.cpp +0 -22
  35. rbx/testdata/caching/executable.py +0 -1
  36. rbx/testdata/compatible +0 -0
  37. rbx/testdata/interactive/checker.cpp +0 -21
  38. rbx/testdata/interactive/gen.cpp +0 -11
  39. rbx/testdata/interactive/interactor.cpp +0 -63
  40. rbx/testdata/interactive/problem.rbx.yml +0 -40
  41. rbx/testdata/interactive/sols/af_ac_pe.cpp +0 -75
  42. rbx/testdata/interactive/sols/af_ac_re.cpp +0 -76
  43. rbx/testdata/interactive/sols/af_ac_too_many_iter.cpp +0 -72
  44. rbx/testdata/interactive/sols/af_inf_cout_with_flush.cpp +0 -79
  45. rbx/testdata/interactive/sols/af_inf_cout_without_flush.cpp +0 -78
  46. rbx/testdata/interactive/sols/af_ml.cpp +0 -78
  47. rbx/testdata/interactive/sols/af_tl_after_ans.cpp +0 -74
  48. rbx/testdata/interactive/sols/af_wa.cpp +0 -74
  49. rbx/testdata/interactive/sols/interactive-binary-search_mm_naive_cin.cpp +0 -17
  50. rbx/testdata/interactive/sols/main.cpp +0 -26
  51. rbx/testdata/interactive/testplan.txt +0 -6
  52. rbx/testdata/interactive/validator.cpp +0 -16
  53. {rbx_cp-0.5.42.dist-info → rbx_cp-0.5.46.dist-info}/LICENSE +0 -0
  54. {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
- interactor_prepared.sandbox_params.set_stdio(stdin=fifo_out, stdout=fifo_in)
514
- solution_prepared.sandbox_params.set_stdio(stdin=fifo_in, stdout=fifo_out)
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
- solution_prepared.sandbox_params.reverse_io = True
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
- with tempfile.TemporaryDirectory() as dir:
275
- output_dir = pathlib.Path(dir)
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
- eval: Evaluation = await run_solution_on_testcase(
278
- main_solution,
279
- main_solution_digest,
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
- if eval.log.stdout_absolute_path is not None:
289
- shutil.copy(eval.log.stdout_absolute_path, testcase.outputPath)
290
- if eval.log.stderr_absolute_path is not None and stderr_path is not None:
291
- shutil.copy(eval.log.stderr_absolute_path, stderr_path)
292
-
293
- if eval.result.outcome != Outcome.ACCEPTED:
294
- console.console.print(
295
- f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
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'Output written at [item]{testcase.outputPath}[/item]'
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
- raise typer.Exit(1)
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
@@ -1,6 +1,2 @@
1
1
  # flake8: noqa
2
- from gevent import monkey
3
-
4
- monkey.patch_all()
5
-
6
2
  from rbx.box.cli import app
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 = get_problem_cache_dir(root) / '.empty'
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 = get_problem_cache_dir(root) / '.fifos'
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
@@ -5,6 +5,7 @@ import dataclasses
5
5
  class State:
6
6
  run_through_cli: bool = False
7
7
  sanitized: bool = False
8
+ debug_logs: bool = False
8
9
 
9
10
 
10
11
  STATE = State()
@@ -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=testcase.inputPath,
71
- outputPath=testcase.outputPath or utils.get_empty_sentinel_path(),
72
- hasOutput=testcase.outputPath is not None,
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
- output_path = output_dir / testcase.inputPath.with_suffix('.out').name
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
- output_path = output_dir / testcase.inputPath.with_suffix('.out').name
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
+ )
@@ -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:]
@@ -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
 
@@ -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