rbx.cp 0.13.4__py3-none-any.whl → 0.13.6__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/checkers.py +2 -9
- rbx/box/cli.py +0 -1
- rbx/box/code.py +27 -80
- rbx/box/environment.py +16 -6
- rbx/box/generators.py +26 -3
- rbx/box/global_package.py +1 -1
- rbx/box/header.py +26 -8
- rbx/box/package.py +0 -14
- rbx/box/setter_config.py +11 -0
- rbx/box/solutions.py +12 -4
- rbx/box/tasks.py +9 -4
- rbx/box/testing/testing_package.py +69 -2
- rbx/box/ui/screens/run_explorer.py +0 -8
- rbx/box/ui/utils/run_ui.py +7 -3
- rbx/box/ui/widgets/test_output_box.py +1 -1
- rbx/box/unit.py +4 -4
- rbx/box/validators.py +3 -1
- rbx/grading/caching.py +65 -15
- rbx/grading/judge/cacher.py +5 -3
- rbx/grading/judge/program.py +300 -0
- rbx/grading/judge/sandbox.py +30 -200
- rbx/grading/judge/sandboxes/stupid_sandbox.py +234 -240
- rbx/grading/judge/sandboxes/tee.py +31 -0
- rbx/grading/judge/storage.py +7 -1
- rbx/grading/steps.py +89 -201
- rbx/grading/steps_with_caching.py +15 -6
- rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
- rbx/resources/templates/rbx.h +43 -2
- rbx/testing_utils.py +7 -0
- rbx/utils.py +104 -6
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/METADATA +1 -1
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/RECORD +35 -40
- rbx/grading/judge/sandboxes/isolate.py +0 -695
- rbx/grading/judge/sandboxes/timeit.py +0 -358
- rbx/grading/judge/test.py +0 -38
- rbx/grading/judge/testiso.py +0 -54
- rbx/grading/processing_context.py +0 -71
- rbx/resources/envs/isolate.rbx.yml +0 -36
- rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/LICENSE +0 -0
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/WHEEL +0 -0
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/entry_points.txt +0 -0
rbx/box/checkers.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import pathlib
|
2
|
-
import signal
|
3
2
|
from typing import List, Optional
|
4
3
|
|
5
4
|
import typer
|
@@ -248,6 +247,7 @@ async def _check(
|
|
248
247
|
console.console.print(
|
249
248
|
f'[error]Summary:[/error] {checker_run_log.get_summary()}'
|
250
249
|
)
|
250
|
+
console.console.print(f'[error]Message:[/error] {message}')
|
251
251
|
console.console.print(
|
252
252
|
f'[error]Testcase input:[/error] [item]{testcase.inputPath}[/item]'
|
253
253
|
)
|
@@ -345,14 +345,7 @@ async def check_communication(
|
|
345
345
|
if (
|
346
346
|
interactor_run_log is not None
|
347
347
|
and run_log is not None
|
348
|
-
and (
|
349
|
-
run_log.exitcode == -signal.SIGPIPE
|
350
|
-
or run_log.exitstatus == SandboxBase.EXIT_TERMINATED
|
351
|
-
or (
|
352
|
-
run_log.exitstatus == SandboxBase.EXIT_NONZERO_RETURN
|
353
|
-
and not _is_testlib_eof(interactor_stderr.read_text())
|
354
|
-
)
|
355
|
-
)
|
348
|
+
and (interactor_run_log.exitindex < run_log.exitindex)
|
356
349
|
):
|
357
350
|
result = _check_interactor()
|
358
351
|
if result is not None and result.outcome != Outcome.ACCEPTED:
|
rbx/box/cli.py
CHANGED
rbx/box/code.py
CHANGED
@@ -12,7 +12,7 @@ import rich.text
|
|
12
12
|
import typer
|
13
13
|
from pydantic import BaseModel
|
14
14
|
|
15
|
-
from rbx import console
|
15
|
+
from rbx import console
|
16
16
|
from rbx.box import download, global_package, package, setter_config, state
|
17
17
|
from rbx.box.environment import (
|
18
18
|
CompilationConfig,
|
@@ -48,6 +48,8 @@ from rbx.grading.steps import (
|
|
48
48
|
maybe_get_bits_stdcpp_for_commands,
|
49
49
|
)
|
50
50
|
|
51
|
+
MERGED_CAPTURE_FILENAME = 'merged_capture.pio'
|
52
|
+
|
51
53
|
|
52
54
|
class SanitizationLevel(Enum):
|
53
55
|
NONE = 0
|
@@ -207,6 +209,9 @@ def _format_stack_limit(limit: int) -> str:
|
|
207
209
|
|
208
210
|
|
209
211
|
def _check_stack_limit():
|
212
|
+
cfg = setter_config.get_setter_config()
|
213
|
+
if not cfg.judging.check_stack:
|
214
|
+
return
|
210
215
|
if not state.STATE.run_through_cli:
|
211
216
|
return
|
212
217
|
soft, hard = resource.RLIM_INFINITY, resource.RLIM_INFINITY
|
@@ -261,52 +266,6 @@ class PreparedRun:
|
|
261
266
|
metadata: RunLogMetadata
|
262
267
|
|
263
268
|
|
264
|
-
@dataclasses.dataclass
|
265
|
-
class CaptureSpec:
|
266
|
-
prefix: str
|
267
|
-
output: Optional[DigestOrDest] = None
|
268
|
-
merged_capture: Optional[pathlib.Path] = None
|
269
|
-
|
270
|
-
|
271
|
-
def _prepare_for_communication(
|
272
|
-
run: PreparedRun,
|
273
|
-
stdin: pathlib.Path,
|
274
|
-
stdout: pathlib.Path,
|
275
|
-
reverse_io: bool = False,
|
276
|
-
capture: Optional[CaptureSpec] = None,
|
277
|
-
):
|
278
|
-
run.sandbox_params.set_stdio(
|
279
|
-
stdin=stdin,
|
280
|
-
stdout=stdout,
|
281
|
-
)
|
282
|
-
run.sandbox_params.reverse_io = reverse_io
|
283
|
-
if capture is not None:
|
284
|
-
run.sandbox_params.timeit_prefix = capture.prefix
|
285
|
-
|
286
|
-
if capture.output is not None:
|
287
|
-
output_path = PosixPath('capture')
|
288
|
-
run.sandbox_params.timeit_dups['do'].append(output_path)
|
289
|
-
|
290
|
-
run.artifacts.outputs.append(
|
291
|
-
GradingFileOutput(
|
292
|
-
src=output_path,
|
293
|
-
**capture.output.expand(),
|
294
|
-
touch=True,
|
295
|
-
)
|
296
|
-
)
|
297
|
-
|
298
|
-
if capture.merged_capture is not None:
|
299
|
-
merged_output_path = utils.abspath(package.get_merged_capture_path())
|
300
|
-
run.sandbox_params.timeit_dups['Do'].append(merged_output_path)
|
301
|
-
|
302
|
-
run.artifacts.outputs.append(
|
303
|
-
GradingFileOutput(
|
304
|
-
src=merged_output_path,
|
305
|
-
dest=capture.merged_capture,
|
306
|
-
)
|
307
|
-
)
|
308
|
-
|
309
|
-
|
310
269
|
def _prepare_run(
|
311
270
|
code: CodeItem,
|
312
271
|
executable: DigestOrSource,
|
@@ -318,12 +277,13 @@ def _prepare_run(
|
|
318
277
|
extra_args: Optional[str] = None,
|
319
278
|
extra_config: Optional[ExecutionConfig] = None,
|
320
279
|
retry_index: Optional[int] = None,
|
280
|
+
file_prefix: Optional[str] = None,
|
321
281
|
):
|
322
282
|
language = find_language_name(code)
|
323
283
|
execution_options = get_execution_config(language)
|
324
284
|
if extra_config is not None:
|
325
285
|
execution_options = merge_execution_configs([execution_options, extra_config])
|
326
|
-
file_mapping = get_file_mapping(language)
|
286
|
+
file_mapping = get_file_mapping(language, file_prefix)
|
327
287
|
sandbox_params = get_sandbox_params_from_config(execution_options.sandbox)
|
328
288
|
|
329
289
|
# Sanitization parameters.
|
@@ -739,6 +699,7 @@ async def run_item(
|
|
739
699
|
class CommunicationItem:
|
740
700
|
code: CodeItem
|
741
701
|
executable: DigestOrSource
|
702
|
+
file_prefix: str
|
742
703
|
stderr: Optional[DigestOrDest] = None
|
743
704
|
inputs: Optional[List[GradingFileInput]] = None
|
744
705
|
outputs: Optional[List[GradingFileOutput]] = None
|
@@ -750,21 +711,22 @@ class CommunicationItem:
|
|
750
711
|
return _prepare_run(
|
751
712
|
self.code,
|
752
713
|
self.executable,
|
714
|
+
stdout=self.capture,
|
753
715
|
stderr=self.stderr,
|
754
716
|
inputs=self.inputs,
|
755
717
|
outputs=self.outputs,
|
756
718
|
extra_args=self.extra_args,
|
757
719
|
extra_config=self.extra_config,
|
720
|
+
file_prefix=self.file_prefix,
|
758
721
|
)
|
759
722
|
|
760
723
|
|
761
724
|
async def run_communication(
|
762
725
|
interactor: CommunicationItem,
|
763
726
|
solution: CommunicationItem,
|
764
|
-
merged_capture: Optional[
|
727
|
+
merged_capture: Optional[DigestOrDest] = None,
|
765
728
|
retry_index: Optional[int] = None,
|
766
729
|
):
|
767
|
-
fifo_in, fifo_out = package.get_fifos()
|
768
730
|
interactor_prepared = interactor.prepare()
|
769
731
|
solution_prepared = solution.prepare()
|
770
732
|
|
@@ -772,48 +734,30 @@ async def run_communication(
|
|
772
734
|
interactor_prepared.metadata.retryIndex = retry_index
|
773
735
|
solution_prepared.metadata.retryIndex = retry_index
|
774
736
|
|
775
|
-
|
776
|
-
|
737
|
+
grading_artifacts = GradingArtifacts()
|
738
|
+
grading_artifacts.inputs.extend(interactor_prepared.artifacts.inputs)
|
739
|
+
grading_artifacts.outputs.extend(interactor_prepared.artifacts.outputs)
|
740
|
+
grading_artifacts.inputs.extend(solution_prepared.artifacts.inputs)
|
741
|
+
grading_artifacts.outputs.extend(solution_prepared.artifacts.outputs)
|
777
742
|
|
743
|
+
merged_capture_path: Optional[pathlib.Path] = None
|
778
744
|
if merged_capture is not None:
|
779
|
-
|
780
|
-
|
745
|
+
merged_capture_path = pathlib.Path(MERGED_CAPTURE_FILENAME)
|
746
|
+
grading_artifacts.outputs.append(
|
747
|
+
GradingFileOutput(
|
748
|
+
src=merged_capture_path,
|
749
|
+
**merged_capture.expand(),
|
750
|
+
)
|
781
751
|
)
|
782
752
|
|
783
|
-
_prepare_for_communication(
|
784
|
-
interactor_prepared,
|
785
|
-
fifo_out,
|
786
|
-
fifo_in,
|
787
|
-
capture=CaptureSpec(
|
788
|
-
prefix=interactor_prefix,
|
789
|
-
output=interactor.capture,
|
790
|
-
merged_capture=merged_capture,
|
791
|
-
),
|
792
|
-
)
|
793
|
-
_prepare_for_communication(
|
794
|
-
solution_prepared,
|
795
|
-
fifo_in,
|
796
|
-
fifo_out,
|
797
|
-
reverse_io=True,
|
798
|
-
capture=CaptureSpec(
|
799
|
-
prefix=solution_prefix,
|
800
|
-
output=solution.capture,
|
801
|
-
merged_capture=merged_capture,
|
802
|
-
),
|
803
|
-
)
|
804
|
-
|
805
753
|
interactor_run_params = steps.CoordinatedRunParams(
|
806
754
|
command=interactor_prepared.command,
|
807
755
|
params=interactor_prepared.sandbox_params,
|
808
|
-
sandbox=package.get_singleton_interactor_sandbox(),
|
809
|
-
artifacts=interactor_prepared.artifacts,
|
810
756
|
metadata=interactor_prepared.metadata,
|
811
757
|
)
|
812
758
|
solution_run_params = steps.CoordinatedRunParams(
|
813
759
|
command=solution_prepared.command,
|
814
760
|
params=solution_prepared.sandbox_params,
|
815
|
-
sandbox=package.get_singleton_sandbox(),
|
816
|
-
artifacts=solution_prepared.artifacts,
|
817
761
|
metadata=solution_prepared.metadata,
|
818
762
|
)
|
819
763
|
|
@@ -825,5 +769,8 @@ async def run_communication(
|
|
825
769
|
return await steps_with_caching.run_coordinated(
|
826
770
|
interactor_run_params,
|
827
771
|
solution_run_params,
|
772
|
+
sandbox=package.get_singleton_sandbox(),
|
773
|
+
artifacts=grading_artifacts,
|
828
774
|
dependency_cache=package.get_dependency_cache(),
|
775
|
+
merged_capture=merged_capture_path,
|
829
776
|
)
|
rbx/box/environment.py
CHANGED
@@ -10,7 +10,6 @@ from rbx import config, console, utils
|
|
10
10
|
from rbx.box import presets
|
11
11
|
from rbx.box.extensions import Extensions, LanguageExtensions
|
12
12
|
from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
|
13
|
-
from rbx.grading.judge.sandboxes.isolate import IsolateSandbox
|
14
13
|
from rbx.grading.judge.sandboxes.stupid_sandbox import StupidSandbox
|
15
14
|
from rbx.grading.limits import Limits
|
16
15
|
|
@@ -58,6 +57,12 @@ relative to the sandbox root.""",
|
|
58
57
|
relative to the sandbox root.""",
|
59
58
|
)
|
60
59
|
|
60
|
+
capture: str = Field(
|
61
|
+
default='capture',
|
62
|
+
description="""Path where to output the capture file after running the program,
|
63
|
+
relative to the sandbox root.""",
|
64
|
+
)
|
65
|
+
|
61
66
|
compilable: str = Field(
|
62
67
|
default='compilable',
|
63
68
|
description="""Path where to copy the compilable file to before compiling the program,
|
@@ -232,7 +237,7 @@ execution config can be individually overridden in the language configuration.""
|
|
232
237
|
|
233
238
|
sandbox: str = Field(
|
234
239
|
default='stupid',
|
235
|
-
description="""Identifier of the sandbox used by this environment (e.g. "stupid"
|
240
|
+
description="""Identifier of the sandbox used by this environment (e.g. "stupid")""",
|
236
241
|
)
|
237
242
|
|
238
243
|
timing: TimingConfig = Field(
|
@@ -381,13 +386,20 @@ def get_execution_config(language: str) -> ExecutionConfig:
|
|
381
386
|
|
382
387
|
|
383
388
|
@functools.cache
|
384
|
-
def get_file_mapping(language: str) -> FileMapping:
|
389
|
+
def get_file_mapping(language: str, file_prefix: Optional[str] = None) -> FileMapping:
|
385
390
|
environment = get_environment()
|
386
|
-
|
391
|
+
mapping = _merge_shallow_models(
|
387
392
|
FileMapping,
|
388
393
|
environment.defaultFileMapping or FileMapping(),
|
389
394
|
get_language(language).fileMapping or FileMapping(),
|
390
395
|
)
|
396
|
+
if file_prefix is not None:
|
397
|
+
mapping.input = f'{file_prefix}_{mapping.input}'
|
398
|
+
mapping.output = f'{file_prefix}_{mapping.output}'
|
399
|
+
mapping.error = f'{file_prefix}_{mapping.error}'
|
400
|
+
mapping.compilable = f'{file_prefix}_{mapping.compilable}'
|
401
|
+
mapping.executable = f'{file_prefix}_{mapping.executable}'
|
402
|
+
return mapping
|
391
403
|
|
392
404
|
|
393
405
|
@functools.cache
|
@@ -395,8 +407,6 @@ def get_sandbox_type() -> Type[SandboxBase]:
|
|
395
407
|
used_sandbox = get_environment().sandbox
|
396
408
|
if used_sandbox == 'stupid':
|
397
409
|
return StupidSandbox
|
398
|
-
if used_sandbox == 'isolate':
|
399
|
-
return IsolateSandbox
|
400
410
|
return StupidSandbox
|
401
411
|
|
402
412
|
|
rbx/box/generators.py
CHANGED
@@ -120,15 +120,25 @@ def _copy_testcase_output_over(
|
|
120
120
|
|
121
121
|
|
122
122
|
def _copy_testcase_outputs_over(
|
123
|
-
testcase: Testcase,
|
123
|
+
testcase: Testcase,
|
124
|
+
dest: Testcase,
|
125
|
+
pipes: bool = False,
|
126
|
+
only_pipes: bool = False,
|
127
|
+
dry_run: bool = False,
|
124
128
|
):
|
129
|
+
if only_pipes:
|
130
|
+
pipes = True
|
125
131
|
assert dest.outputPath is not None
|
126
132
|
if not dry_run:
|
127
133
|
dest.outputPath.parent.mkdir(parents=True, exist_ok=True)
|
128
134
|
|
129
135
|
has_copied = False
|
130
136
|
|
131
|
-
if
|
137
|
+
if (
|
138
|
+
not only_pipes
|
139
|
+
and testcase.outputPath is not None
|
140
|
+
and testcase.outputPath.is_file()
|
141
|
+
):
|
132
142
|
if not dry_run:
|
133
143
|
_check_crlf(testcase.outputPath)
|
134
144
|
shutil.copy(str(testcase.outputPath), str(dest.outputPath))
|
@@ -371,6 +381,7 @@ async def generate_output_for_testcase(
|
|
371
381
|
main_solution_digest: str,
|
372
382
|
testcase: Testcase,
|
373
383
|
interactor_digest: Optional[str] = None,
|
384
|
+
capture_pipes: Optional[bool] = None,
|
374
385
|
):
|
375
386
|
assert testcase.outputPath is not None
|
376
387
|
testcase.inputPath.parent.mkdir(parents=True, exist_ok=True)
|
@@ -388,7 +399,7 @@ async def generate_output_for_testcase(
|
|
388
399
|
interactor_digest=interactor_digest,
|
389
400
|
use_retries=False,
|
390
401
|
use_timelimit=False,
|
391
|
-
capture_pipes=
|
402
|
+
capture_pipes=capture_pipes,
|
392
403
|
)
|
393
404
|
|
394
405
|
if eval.result.outcome.is_slow() and eval.result.no_tle_outcome == Outcome.ACCEPTED:
|
@@ -474,10 +485,22 @@ async def generate_outputs_for_testcases(
|
|
474
485
|
raise typer.Exit(1)
|
475
486
|
|
476
487
|
assert solution_digest is not None
|
488
|
+
capture_pipes = None
|
489
|
+
if (
|
490
|
+
pkg.type == TaskType.COMMUNICATION
|
491
|
+
and entry.metadata.copied_from is not None
|
492
|
+
):
|
493
|
+
# If some pipe file is already specified, we don't need to capture the pipes
|
494
|
+
# when running the program.
|
495
|
+
capture_pipes = not _copy_testcase_outputs_over(
|
496
|
+
entry.metadata.copied_from, tc, only_pipes=True, dry_run=True
|
497
|
+
)
|
498
|
+
|
477
499
|
await generate_output_for_testcase(
|
478
500
|
solution_digest,
|
479
501
|
tc,
|
480
502
|
interactor_digest=interactor_digest,
|
503
|
+
capture_pipes=capture_pipes,
|
481
504
|
)
|
482
505
|
if entry.metadata.copied_from is not None:
|
483
506
|
# Copy remaining pipe files.
|
rbx/box/global_package.py
CHANGED
@@ -9,7 +9,7 @@ from rbx.grading.judge.sandbox import SandboxBase
|
|
9
9
|
from rbx.grading.judge.sandboxes.stupid_sandbox import StupidSandbox
|
10
10
|
from rbx.grading.judge.storage import FilesystemStorage, Storage
|
11
11
|
|
12
|
-
CACHE_STEP_VERSION =
|
12
|
+
CACHE_STEP_VERSION = 4
|
13
13
|
|
14
14
|
|
15
15
|
def get_cache_fingerprint() -> str:
|
rbx/box/header.py
CHANGED
@@ -49,13 +49,33 @@ def _get_string_var_block() -> str:
|
|
49
49
|
return _get_var_block(_get_vars_of_type(str, _string_repr))
|
50
50
|
|
51
51
|
|
52
|
+
def check_int_bounds(x: int) -> None:
|
53
|
+
if x >= 2**64:
|
54
|
+
raise ValueError(
|
55
|
+
f'Some variable you defined (value: {x}) is too large to fit in a C++ 64-bit integer (signed or unsigned)'
|
56
|
+
)
|
57
|
+
if x < -(2**63):
|
58
|
+
raise ValueError(
|
59
|
+
f'Some variable you defined (value: {x}) is too small to fit in a C++ 64-bit signed integer (int64_t)'
|
60
|
+
)
|
61
|
+
|
62
|
+
|
52
63
|
def _get_int_var_block() -> str:
|
53
64
|
def _transform(x: Primitive) -> str:
|
54
65
|
if isinstance(x, bool):
|
55
|
-
return
|
56
|
-
|
66
|
+
return f'static_cast<int64_t>({int(x)})'
|
67
|
+
check_int_bounds(int(x))
|
68
|
+
return f'static_cast<int64_t>({x})'
|
57
69
|
|
58
|
-
|
70
|
+
# Get both int and bool variables for the int block
|
71
|
+
pkg = package.find_problem_package_or_die()
|
72
|
+
vars = pkg.expanded_vars
|
73
|
+
int_vars = {
|
74
|
+
name: _transform(value)
|
75
|
+
for name, value in vars.items()
|
76
|
+
if isinstance(value, (int, bool))
|
77
|
+
}
|
78
|
+
return _get_var_block(int_vars)
|
59
79
|
|
60
80
|
|
61
81
|
def _get_float_var_block() -> str:
|
@@ -69,11 +89,9 @@ def _get_bool_var_block() -> str:
|
|
69
89
|
def _get_vars_of_type(t: Type, transform: Callable[[Primitive], str]) -> Dict[str, str]:
|
70
90
|
pkg = package.find_problem_package_or_die()
|
71
91
|
vars = pkg.expanded_vars
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
return {name: transform(value) for name, value in vars.items() if is_valid(value)}
|
92
|
+
return {
|
93
|
+
name: transform(value) for name, value in vars.items() if isinstance(value, t)
|
94
|
+
}
|
77
95
|
|
78
96
|
|
79
97
|
def _get_var_block(mappings: Dict[str, str]) -> str:
|
rbx/box/package.py
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
import atexit
|
2
2
|
import functools
|
3
|
-
import os
|
4
3
|
import pathlib
|
5
|
-
import shutil
|
6
4
|
import sys
|
7
5
|
from typing import Dict, List, Optional, Tuple
|
8
6
|
|
@@ -455,18 +453,6 @@ def get_empty_sentinel_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
|
|
455
453
|
return path
|
456
454
|
|
457
455
|
|
458
|
-
@functools.cache
|
459
|
-
def get_fifos(root: pathlib.Path = pathlib.Path()) -> Tuple[pathlib.Path, pathlib.Path]:
|
460
|
-
path = get_shared_dir(root) / '.fifos'
|
461
|
-
shutil.rmtree(path, ignore_errors=True)
|
462
|
-
path.mkdir(parents=True, exist_ok=True)
|
463
|
-
fifo_in = path / 'fifo.in'
|
464
|
-
fifo_out = path / 'fifo.out'
|
465
|
-
os.mkfifo(fifo_in)
|
466
|
-
os.mkfifo(fifo_out)
|
467
|
-
return fifo_in, fifo_out
|
468
|
-
|
469
|
-
|
470
456
|
@functools.cache
|
471
457
|
def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
|
472
458
|
path = get_shared_dir(root) / '.merged_capture'
|
rbx/box/setter_config.py
CHANGED
@@ -66,6 +66,13 @@ class CachingConfig(BaseModel):
|
|
66
66
|
)
|
67
67
|
|
68
68
|
|
69
|
+
class JudgingConfig(BaseModel):
|
70
|
+
check_stack: bool = Field(
|
71
|
+
default=True,
|
72
|
+
description='Whether to check the stack size before running code.',
|
73
|
+
)
|
74
|
+
|
75
|
+
|
69
76
|
class SetterConfig(BaseModel):
|
70
77
|
sanitizers: SanitizersConfig = Field(
|
71
78
|
default_factory=SanitizersConfig, # type: ignore
|
@@ -93,6 +100,10 @@ class SetterConfig(BaseModel):
|
|
93
100
|
default_factory=CachingConfig, # type: ignore
|
94
101
|
description='Configuration for caching.',
|
95
102
|
)
|
103
|
+
judging: JudgingConfig = Field(
|
104
|
+
default_factory=JudgingConfig, # type: ignore
|
105
|
+
description='Configuration for judging.',
|
106
|
+
)
|
96
107
|
|
97
108
|
def substitute_command(self, command: str, sanitized: bool = False) -> str:
|
98
109
|
exe = shlex.split(command)[0]
|
rbx/box/solutions.py
CHANGED
@@ -922,6 +922,12 @@ def get_worst_outcome(evals: List[Evaluation]) -> Outcome:
|
|
922
922
|
return Outcome.worst_outcome(eval.result.outcome for eval in evals)
|
923
923
|
|
924
924
|
|
925
|
+
def get_truncated_message(message: str, max_length: int = 100) -> str:
|
926
|
+
if len(message) > max_length:
|
927
|
+
return message[:max_length] + '... (truncated)'
|
928
|
+
return message
|
929
|
+
|
930
|
+
|
925
931
|
class SolutionOutcomeReport(BaseModel):
|
926
932
|
solution: Solution
|
927
933
|
evals: List[Evaluation]
|
@@ -971,9 +977,8 @@ class SolutionOutcomeReport(BaseModel):
|
|
971
977
|
if print_message and self.message is not None:
|
972
978
|
tc, msg = self.message
|
973
979
|
if msg:
|
974
|
-
|
975
|
-
|
976
|
-
res += f'\nMessage for {tc}: {msg}'
|
980
|
+
msg = get_truncated_message(msg)
|
981
|
+
res += f'\nMessage for {utils.escape_markup(str(tc))}: {utils.escape_markup(msg)}'
|
977
982
|
return res
|
978
983
|
|
979
984
|
|
@@ -1471,7 +1476,10 @@ async def print_run_report(
|
|
1471
1476
|
console.print(f' ({time}, {memory})', end='')
|
1472
1477
|
checker_msg = eval.result.message
|
1473
1478
|
if checker_msg:
|
1474
|
-
|
1479
|
+
checker_msg = get_truncated_message(checker_msg, 150)
|
1480
|
+
console.print(
|
1481
|
+
f': [i]{utils.escape_markup(checker_msg)}[/i]', end=''
|
1482
|
+
)
|
1475
1483
|
else:
|
1476
1484
|
console.print(f'{i}/', end='')
|
1477
1485
|
console.print(get_testcase_markup_verdict(eval), end='')
|
rbx/box/tasks.py
CHANGED
@@ -51,7 +51,7 @@ async def run_solution_on_testcase(
|
|
51
51
|
timelimit_override: Optional[int] = None,
|
52
52
|
use_retries: bool = True,
|
53
53
|
use_timelimit: bool = True,
|
54
|
-
capture_pipes: bool =
|
54
|
+
capture_pipes: Optional[bool] = None,
|
55
55
|
nruns: int = 0,
|
56
56
|
filestem: Optional[str] = None,
|
57
57
|
is_stress: bool = False,
|
@@ -175,12 +175,13 @@ async def _run_communication_solution_on_testcase(
|
|
175
175
|
timelimit_override: Optional[int] = None,
|
176
176
|
use_retries: bool = True,
|
177
177
|
use_timelimit: bool = True,
|
178
|
-
capture_pipes: bool =
|
178
|
+
capture_pipes: Optional[bool] = None,
|
179
179
|
nruns: int = 0,
|
180
180
|
filestem: Optional[str] = None,
|
181
181
|
is_stress: bool = False,
|
182
182
|
) -> Evaluation:
|
183
|
-
|
183
|
+
if capture_pipes is None:
|
184
|
+
capture_pipes = state.STATE.debug_logs
|
184
185
|
|
185
186
|
async def run_fn(retry_index: int) -> Evaluation:
|
186
187
|
actual_sandbox = package.get_singleton_sandbox()
|
@@ -243,6 +244,7 @@ async def _run_communication_solution_on_testcase(
|
|
243
244
|
capture=DigestOrDest.create(interactor_capture_path)
|
244
245
|
if interactor_capture_path
|
245
246
|
else None,
|
247
|
+
file_prefix='interactor',
|
246
248
|
)
|
247
249
|
solution_capture_path = (
|
248
250
|
output_path.with_suffix('.pout') if capture_pipes else None
|
@@ -255,6 +257,7 @@ async def _run_communication_solution_on_testcase(
|
|
255
257
|
capture=DigestOrDest.create(solution_capture_path)
|
256
258
|
if solution_capture_path
|
257
259
|
else None,
|
260
|
+
file_prefix='solution',
|
258
261
|
)
|
259
262
|
|
260
263
|
merged_capture_path = output_path.with_suffix('.pio') if capture_pipes else None
|
@@ -262,7 +265,9 @@ async def _run_communication_solution_on_testcase(
|
|
262
265
|
interactor=interactor_item,
|
263
266
|
solution=solution_item,
|
264
267
|
retry_index=retry_index,
|
265
|
-
merged_capture=merged_capture_path
|
268
|
+
merged_capture=DigestOrDest.create(merged_capture_path)
|
269
|
+
if merged_capture_path
|
270
|
+
else None,
|
266
271
|
)
|
267
272
|
|
268
273
|
checker_result = await checkers.check_communication(
|
@@ -3,8 +3,10 @@ from dataclasses import dataclass
|
|
3
3
|
from typing import Dict, List, Optional
|
4
4
|
|
5
5
|
from rbx import console, utils
|
6
|
+
from rbx.box import package, presets
|
6
7
|
from rbx.box.fields import Primitive
|
7
8
|
from rbx.box.schema import (
|
9
|
+
CheckerTest,
|
8
10
|
CodeItem,
|
9
11
|
ExpectedOutcome,
|
10
12
|
Generator,
|
@@ -13,6 +15,8 @@ from rbx.box.schema import (
|
|
13
15
|
Solution,
|
14
16
|
TaskType,
|
15
17
|
TestcaseGroup,
|
18
|
+
ValidatorOutcome,
|
19
|
+
ValidatorTest,
|
16
20
|
)
|
17
21
|
from rbx.box.testing.testing_preset import TestingPreset
|
18
22
|
from rbx.box.testing.testing_shared import PathOrStr, TestingShared
|
@@ -52,8 +56,12 @@ class TestingPackage(TestingShared):
|
|
52
56
|
)
|
53
57
|
|
54
58
|
def initialize_preset(self) -> TestingPreset:
|
55
|
-
|
56
|
-
|
59
|
+
preset = presets.get_active_preset_or_null(self.root)
|
60
|
+
if preset is None:
|
61
|
+
preset_path = self.root / '.local.rbx'
|
62
|
+
preset_path.mkdir(parents=True, exist_ok=True)
|
63
|
+
else:
|
64
|
+
preset_path = presets.get_active_preset_path(self.root)
|
57
65
|
return TestingPreset(preset_path)
|
58
66
|
|
59
67
|
def print_tree(self):
|
@@ -74,6 +82,9 @@ class TestingPackage(TestingShared):
|
|
74
82
|
|
75
83
|
def save(self):
|
76
84
|
self.yml_path.write_text(utils.model_to_yaml(self.yml))
|
85
|
+
# Clear internal cache and package cache to ensure the updated package is loaded fresh
|
86
|
+
self._yml = None
|
87
|
+
package.clear_package_cache()
|
77
88
|
|
78
89
|
def set_type(self, type: TaskType):
|
79
90
|
self.yml.type = type
|
@@ -239,3 +250,59 @@ class TestingPackage(TestingShared):
|
|
239
250
|
if interactor_pipes_path.exists():
|
240
251
|
contents.interactor_pipes = interactor_pipes_path.read_text()
|
241
252
|
return contents
|
253
|
+
|
254
|
+
def add_validator_unit_test(
|
255
|
+
self,
|
256
|
+
glob: str,
|
257
|
+
outcome: ValidatorOutcome = ValidatorOutcome.VALID,
|
258
|
+
validator: Optional[PathOrStr] = None,
|
259
|
+
files: Optional[Dict[str, str]] = None,
|
260
|
+
):
|
261
|
+
"""Add a unit test for the validator.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
glob: Glob pattern for input files
|
265
|
+
outcome: Expected validation outcome
|
266
|
+
validator: Optional validator to use (if not main validator)
|
267
|
+
files: Optional dict of {filename: content} to create test files
|
268
|
+
"""
|
269
|
+
if files:
|
270
|
+
for filename, content in files.items():
|
271
|
+
self.add_file(filename).write_text(content)
|
272
|
+
|
273
|
+
validator_test = ValidatorTest(
|
274
|
+
glob=glob,
|
275
|
+
outcome=outcome,
|
276
|
+
validator=CodeItem(path=pathlib.Path(validator)) if validator else None,
|
277
|
+
)
|
278
|
+
|
279
|
+
# Explicitly set the unitTests field to mark it as dirty
|
280
|
+
unit_tests = self.yml.unitTests
|
281
|
+
unit_tests.validator = unit_tests.validator + [validator_test]
|
282
|
+
self.yml.unitTests = unit_tests
|
283
|
+
self.save()
|
284
|
+
|
285
|
+
def add_checker_unit_test(
|
286
|
+
self,
|
287
|
+
glob: str,
|
288
|
+
outcome: ExpectedOutcome = ExpectedOutcome.ACCEPTED,
|
289
|
+
files: Optional[Dict[str, str]] = None,
|
290
|
+
):
|
291
|
+
"""Add a unit test for the checker.
|
292
|
+
|
293
|
+
Args:
|
294
|
+
glob: Glob pattern for test files
|
295
|
+
outcome: Expected checker outcome
|
296
|
+
files: Optional dict of {filename: content} to create test files
|
297
|
+
"""
|
298
|
+
if files:
|
299
|
+
for filename, content in files.items():
|
300
|
+
self.add_file(filename).write_text(content)
|
301
|
+
|
302
|
+
checker_test = CheckerTest(glob=glob, outcome=outcome)
|
303
|
+
|
304
|
+
# Explicitly set the unitTests field to mark it as dirty
|
305
|
+
unit_tests = self.yml.unitTests
|
306
|
+
unit_tests.checker = unit_tests.checker + [checker_test]
|
307
|
+
self.yml.unitTests = unit_tests
|
308
|
+
self.save()
|