rbx.cp 0.13.3__py3-none-any.whl → 0.13.5__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/annotations.py +5 -5
- rbx/box/checkers.py +26 -22
- rbx/box/cli.py +0 -4
- rbx/box/code.py +27 -80
- rbx/box/contest/build_contest_statements.py +16 -3
- rbx/box/contest/schema.py +1 -2
- rbx/box/environment.py +16 -6
- rbx/box/fields.py +25 -1
- rbx/box/generators.py +31 -5
- rbx/box/global_package.py +6 -2
- rbx/box/header.py +31 -11
- rbx/box/package.py +3 -15
- rbx/box/presets/__init__.py +2 -2
- rbx/box/schema.py +4 -25
- rbx/box/setter_config.py +11 -0
- rbx/box/solutions.py +12 -4
- rbx/box/statements/build_statements.py +5 -1
- rbx/box/statements/builders.py +7 -7
- rbx/box/statements/schema.py +11 -2
- rbx/box/tasks.py +9 -4
- rbx/box/testcase_utils.py +2 -0
- rbx/box/testing/__init__.py +0 -0
- rbx/box/testing/testing_package.py +246 -0
- rbx/box/testing/testing_preset.py +36 -0
- rbx/box/testing/testing_shared.py +81 -0
- 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/validators.py +5 -2
- rbx/grading/caching.py +67 -16
- rbx/grading/judge/program.py +268 -0
- rbx/grading/judge/sandbox.py +30 -193
- rbx/grading/judge/sandboxes/stupid_sandbox.py +232 -241
- rbx/grading/judge/sandboxes/tee.py +31 -0
- rbx/grading/steps.py +87 -199
- rbx/grading/steps_with_caching.py +15 -6
- rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
- rbx/resources/presets/default/shared/contest_template.rbx.tex +1 -1
- rbx/resources/presets/default/shared/problem_template.rbx.tex +5 -1
- rbx/resources/templates/rbx.h +43 -2
- rbx/testing_utils.py +8 -1
- rbx/utils.py +59 -1
- {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/METADATA +2 -1
- {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/RECORD +47 -67
- rbx/box/conftest.py +0 -42
- rbx/box/generators_test.py +0 -67
- rbx/box/lazy_importing_test.py +0 -25
- rbx/box/solutions_test.py +0 -47
- rbx/box/validators_test.py +0 -15
- rbx/checker.py +0 -128
- rbx/clone.py +0 -197
- rbx/conftest.py +0 -38
- rbx/create.py +0 -37
- rbx/edit.py +0 -24
- rbx/grading/conftest.py +0 -33
- rbx/grading/judge/sandboxes/isolate.py +0 -695
- rbx/grading/judge/testiso.py +0 -54
- rbx/grading/steps_with_caching_run_test.py +0 -707
- rbx/grading_utils.py +0 -148
- rbx/hydration.py +0 -101
- rbx/main.py +0 -118
- rbx/metadata.py +0 -105
- rbx/resources/envs/isolate.rbx.yml +0 -36
- rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
- rbx/run.py +0 -45
- rbx/schema.py +0 -64
- rbx/submit.py +0 -61
- rbx/test.py +0 -349
- rbx/testcase.py +0 -70
- rbx/testcase_rendering.py +0 -79
- {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/LICENSE +0 -0
- {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/WHEEL +0 -0
- {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/entry_points.txt +0 -0
rbx/box/header.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
import functools
|
2
2
|
import importlib
|
3
3
|
import importlib.resources
|
4
|
+
import json
|
4
5
|
import pathlib
|
5
6
|
from typing import Callable, Dict, Type
|
6
7
|
|
7
8
|
from rbx.box import package
|
8
|
-
from rbx.box.
|
9
|
+
from rbx.box.fields import Primitive
|
9
10
|
|
10
11
|
|
11
12
|
@functools.cache
|
@@ -40,12 +41,33 @@ def _preprocess_header(header: str) -> str:
|
|
40
41
|
)
|
41
42
|
|
42
43
|
|
44
|
+
def _string_repr(s):
|
45
|
+
return json.dumps(s)
|
46
|
+
|
47
|
+
|
43
48
|
def _get_string_var_block() -> str:
|
44
|
-
return _get_var_block(_get_vars_of_type(str,
|
49
|
+
return _get_var_block(_get_vars_of_type(str, _string_repr))
|
50
|
+
|
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
|
+
)
|
45
61
|
|
46
62
|
|
47
63
|
def _get_int_var_block() -> str:
|
48
|
-
|
64
|
+
def _transform(x: Primitive) -> str:
|
65
|
+
if isinstance(x, bool):
|
66
|
+
return str(int(x))
|
67
|
+
_check_int_bounds(int(x))
|
68
|
+
return f'static_cast<int64_t>({x})'
|
69
|
+
|
70
|
+
return _get_var_block(_get_vars_of_type(int, _transform))
|
49
71
|
|
50
72
|
|
51
73
|
def _get_float_var_block() -> str:
|
@@ -56,16 +78,14 @@ def _get_bool_var_block() -> str:
|
|
56
78
|
return _get_var_block(_get_vars_of_type(bool, lambda x: 'true' if x else 'false'))
|
57
79
|
|
58
80
|
|
59
|
-
def _get_vars_of_type(
|
60
|
-
type: Type, transform: Callable[[Primitive], str]
|
61
|
-
) -> Dict[str, str]:
|
81
|
+
def _get_vars_of_type(t: Type, transform: Callable[[Primitive], str]) -> Dict[str, str]:
|
62
82
|
pkg = package.find_problem_package_or_die()
|
63
83
|
vars = pkg.expanded_vars
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
}
|
84
|
+
|
85
|
+
def is_valid(value: Primitive) -> bool:
|
86
|
+
return isinstance(value, t)
|
87
|
+
|
88
|
+
return {name: transform(value) for name, value in vars.items() if is_valid(value)}
|
69
89
|
|
70
90
|
|
71
91
|
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
|
|
@@ -181,8 +179,10 @@ def get_file_cacher(root: pathlib.Path = pathlib.Path()) -> FileCacher:
|
|
181
179
|
|
182
180
|
@functools.cache
|
183
181
|
def get_digest_as_string(
|
184
|
-
digest: str, root: pathlib.Path = pathlib.Path()
|
182
|
+
digest: Optional[str], root: pathlib.Path = pathlib.Path()
|
185
183
|
) -> Optional[str]:
|
184
|
+
if not digest:
|
185
|
+
return None
|
186
186
|
cacher = get_file_cacher(root)
|
187
187
|
try:
|
188
188
|
content = cacher.get_file_content(digest)
|
@@ -453,18 +453,6 @@ def get_empty_sentinel_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
|
|
453
453
|
return path
|
454
454
|
|
455
455
|
|
456
|
-
@functools.cache
|
457
|
-
def get_fifos(root: pathlib.Path = pathlib.Path()) -> Tuple[pathlib.Path, pathlib.Path]:
|
458
|
-
path = get_shared_dir(root) / '.fifos'
|
459
|
-
shutil.rmtree(path, ignore_errors=True)
|
460
|
-
path.mkdir(parents=True, exist_ok=True)
|
461
|
-
fifo_in = path / 'fifo.in'
|
462
|
-
fifo_out = path / 'fifo.out'
|
463
|
-
os.mkfifo(fifo_in)
|
464
|
-
os.mkfifo(fifo_out)
|
465
|
-
return fifo_in, fifo_out
|
466
|
-
|
467
|
-
|
468
456
|
@functools.cache
|
469
457
|
def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
|
470
458
|
path = get_shared_dir(root) / '.merged_capture'
|
rbx/box/presets/__init__.py
CHANGED
@@ -339,9 +339,9 @@ def _copy_preset_file(
|
|
339
339
|
|
340
340
|
# The symlink points somewhere inside the preset folder, fix the symlink.
|
341
341
|
dst_absolute_path = utils.abspath(dst)
|
342
|
-
fixed_target_relative_path =
|
342
|
+
fixed_target_relative_path = utils.relpath(
|
343
|
+
target_absolute_path,
|
343
344
|
dst_absolute_path.parent,
|
344
|
-
walk_up=True,
|
345
345
|
)
|
346
346
|
dst.symlink_to(fixed_target_relative_path)
|
347
347
|
|
rbx/box/schema.py
CHANGED
@@ -3,19 +3,17 @@ from __future__ import annotations
|
|
3
3
|
import os
|
4
4
|
import pathlib
|
5
5
|
import re
|
6
|
-
from typing import Annotated, Any, Dict, List, Optional
|
6
|
+
from typing import Annotated, Any, Dict, List, Optional
|
7
7
|
|
8
8
|
from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
|
9
9
|
from pydantic_core import PydanticCustomError
|
10
10
|
|
11
11
|
from rbx.autoenum import AutoEnum, alias
|
12
|
-
from rbx.box.fields import NameField
|
12
|
+
from rbx.box.fields import NameField, Primitive, expand_vars
|
13
13
|
from rbx.box.statements.expander import expand_statements
|
14
14
|
from rbx.box.statements.schema import Statement
|
15
15
|
from rbx.grading.steps import Outcome
|
16
16
|
|
17
|
-
Primitive = Union[str, int, float, bool]
|
18
|
-
|
19
17
|
|
20
18
|
def _check_oneof(model_obj: BaseModel, fields: List[str]):
|
21
19
|
has = []
|
@@ -30,27 +28,6 @@ def _check_oneof(model_obj: BaseModel, fields: List[str]):
|
|
30
28
|
)
|
31
29
|
|
32
30
|
|
33
|
-
def expand_var(value: Primitive) -> Primitive:
|
34
|
-
if not isinstance(value, str):
|
35
|
-
return value
|
36
|
-
if value.startswith('\\'):
|
37
|
-
return value[1:]
|
38
|
-
if not value.startswith('py`') or not value.endswith('`'):
|
39
|
-
return value
|
40
|
-
res = eval(value[3:-1])
|
41
|
-
for supported_type in [str, int, float, bool]:
|
42
|
-
if isinstance(res, supported_type):
|
43
|
-
return res
|
44
|
-
|
45
|
-
raise TypeError(
|
46
|
-
f'Variable with backticks should evaluate to a primitive Python type: {value}'
|
47
|
-
)
|
48
|
-
|
49
|
-
|
50
|
-
def expand_vars(vars: Dict[str, Primitive]) -> Dict[str, Primitive]:
|
51
|
-
return {key: expand_var(value) for key, value in vars.items()}
|
52
|
-
|
53
|
-
|
54
31
|
def _represents_int(s: str) -> bool:
|
55
32
|
return re.match(r'[-+]?\d+$', s.strip()) is not None
|
56
33
|
|
@@ -245,6 +222,8 @@ Whether this interactor is a legacy interactor and needs a checker to be specifi
|
|
245
222
|
|
246
223
|
|
247
224
|
class Testcase(BaseModel):
|
225
|
+
__test__ = False
|
226
|
+
|
248
227
|
model_config = ConfigDict(extra='forbid')
|
249
228
|
|
250
229
|
inputPath: pathlib.Path = Field(description="""The path of the input file.""")
|
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='')
|
@@ -262,7 +262,6 @@ def build_statement_bytes(
|
|
262
262
|
languages=get_environment_languages_for_statement(),
|
263
263
|
params=params,
|
264
264
|
root=pathlib.Path(td),
|
265
|
-
custom_vars=custom_vars,
|
266
265
|
),
|
267
266
|
item=StatementBuilderProblem(
|
268
267
|
package=pkg,
|
@@ -271,6 +270,11 @@ def build_statement_bytes(
|
|
271
270
|
get_samples() if use_samples else []
|
272
271
|
),
|
273
272
|
short_name=short_name,
|
273
|
+
vars={
|
274
|
+
**pkg.expanded_vars,
|
275
|
+
**statement.expanded_vars,
|
276
|
+
**(custom_vars or {}),
|
277
|
+
},
|
274
278
|
),
|
275
279
|
verbose=False,
|
276
280
|
)
|
rbx/box/statements/builders.py
CHANGED
@@ -11,7 +11,8 @@ import typer
|
|
11
11
|
from pydantic import BaseModel
|
12
12
|
|
13
13
|
from rbx import console, utils
|
14
|
-
from rbx.box.
|
14
|
+
from rbx.box.fields import Primitive
|
15
|
+
from rbx.box.schema import Package, Testcase
|
15
16
|
from rbx.box.statements.latex_jinja import (
|
16
17
|
JinjaDictWrapper,
|
17
18
|
render_latex_template,
|
@@ -48,8 +49,6 @@ class StatementBuilderContext:
|
|
48
49
|
languages: List[StatementCodeLanguage]
|
49
50
|
params: ConversionStep
|
50
51
|
root: pathlib.Path
|
51
|
-
custom_vars: Optional[Dict[str, Any]] = None
|
52
|
-
vars: Optional[Dict[str, Primitive]] = None
|
53
52
|
|
54
53
|
def build_jinja_kwargs(self) -> Dict[str, Any]:
|
55
54
|
res = {
|
@@ -57,9 +56,6 @@ class StatementBuilderContext:
|
|
57
56
|
'languages': self.languages,
|
58
57
|
'keyed_languages': {lang.id: lang for lang in self.languages},
|
59
58
|
}
|
60
|
-
if self.vars is not None or self.custom_vars is not None:
|
61
|
-
res['vars'] = self.vars or {}
|
62
|
-
res['vars'].update(self.custom_vars or {})
|
63
59
|
return res
|
64
60
|
|
65
61
|
|
@@ -125,12 +121,14 @@ class StatementBuilderProblem(StatementBuilderItem):
|
|
125
121
|
# Will only be filled by contests.
|
126
122
|
io_path: Optional[pathlib.Path] = None
|
127
123
|
|
124
|
+
vars: Optional[Dict[str, Primitive]] = None
|
125
|
+
|
128
126
|
def build_inner_jinja_kwargs(self) -> Dict[str, Any]:
|
129
127
|
kwargs = {
|
130
128
|
'package': self.package,
|
131
129
|
'statement': self.statement,
|
132
130
|
'samples': self.samples,
|
133
|
-
'vars': JinjaDictWrapper(self.
|
131
|
+
'vars': JinjaDictWrapper(self.vars or {}, key='vars'),
|
134
132
|
'title': self.statement.title or self.package.name,
|
135
133
|
}
|
136
134
|
if self.short_name is not None:
|
@@ -152,6 +150,7 @@ class StatementBuilderContest(StatementBuilderItem):
|
|
152
150
|
location: Optional[str] = None
|
153
151
|
date: Optional[str] = None
|
154
152
|
problems: List[StatementBuilderProblem] = dataclasses.field(default_factory=list)
|
153
|
+
vars: Optional[Dict[str, Primitive]] = None
|
155
154
|
|
156
155
|
def build_inner_jinja_kwargs(self) -> Dict[str, Any]:
|
157
156
|
res = {'title': self.title}
|
@@ -167,6 +166,7 @@ class StatementBuilderContest(StatementBuilderItem):
|
|
167
166
|
'problems': [
|
168
167
|
problem.build_inner_jinja_kwargs() for problem in self.problems
|
169
168
|
],
|
169
|
+
'vars': JinjaDictWrapper(self.vars or {}, key='vars'),
|
170
170
|
}
|
171
171
|
return res
|
172
172
|
|
rbx/box/statements/schema.py
CHANGED
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import pathlib
|
4
4
|
from enum import Enum
|
5
|
-
from typing import Annotated, List, Literal, Optional, Union
|
5
|
+
from typing import Annotated, Dict, List, Literal, Optional, Union
|
6
6
|
|
7
7
|
from pydantic import AfterValidator, BaseModel, ConfigDict, Field
|
8
8
|
|
9
9
|
from rbx.autoenum import AutoEnum, alias
|
10
|
-
from rbx.box.fields import FNameField
|
10
|
+
from rbx.box.fields import FNameField, Primitive, expand_var
|
11
11
|
from rbx.box.lang import is_valid_lang_code
|
12
12
|
|
13
13
|
|
@@ -175,3 +175,12 @@ the statement. Files will be included in the same folder as the statement file,
|
|
175
175
|
their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
|
176
176
|
""",
|
177
177
|
)
|
178
|
+
|
179
|
+
vars: Dict[str, Primitive] = Field(
|
180
|
+
default={},
|
181
|
+
description='Variables to be used in the statement.',
|
182
|
+
)
|
183
|
+
|
184
|
+
@property
|
185
|
+
def expanded_vars(self) -> Dict[str, Primitive]:
|
186
|
+
return {key: expand_var(value) for key, value in self.vars.items()}
|
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(
|
rbx/box/testcase_utils.py
CHANGED
File without changes
|
@@ -0,0 +1,246 @@
|
|
1
|
+
import pathlib
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Dict, List, Optional
|
4
|
+
|
5
|
+
from rbx import console, utils
|
6
|
+
from rbx.box import presets
|
7
|
+
from rbx.box.fields import Primitive
|
8
|
+
from rbx.box.schema import (
|
9
|
+
CodeItem,
|
10
|
+
ExpectedOutcome,
|
11
|
+
Generator,
|
12
|
+
Interactor,
|
13
|
+
Package,
|
14
|
+
Solution,
|
15
|
+
TaskType,
|
16
|
+
TestcaseGroup,
|
17
|
+
)
|
18
|
+
from rbx.box.testing.testing_preset import TestingPreset
|
19
|
+
from rbx.box.testing.testing_shared import PathOrStr, TestingShared
|
20
|
+
from rbx.grading.steps import Evaluation
|
21
|
+
from rbx.testing_utils import print_directory_tree
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class TestcaseArtifacts:
|
26
|
+
output: Optional[str] = None
|
27
|
+
log: Optional[Evaluation] = None
|
28
|
+
interactor_input: Optional[str] = None
|
29
|
+
interactor_output: Optional[str] = None
|
30
|
+
interactor_pipes: Optional[str] = None
|
31
|
+
|
32
|
+
|
33
|
+
class TestingPackage(TestingShared):
|
34
|
+
def __init__(self, root: PathOrStr):
|
35
|
+
super().__init__(root)
|
36
|
+
self._yml = None
|
37
|
+
|
38
|
+
self.initialize()
|
39
|
+
self.preset = self.initialize_preset()
|
40
|
+
|
41
|
+
@property
|
42
|
+
def yml_path(self) -> pathlib.Path:
|
43
|
+
return self.root / 'problem.rbx.yml'
|
44
|
+
|
45
|
+
def initialize(self):
|
46
|
+
if not self.yml_path.exists():
|
47
|
+
self.yml_path.parent.mkdir(parents=True, exist_ok=True)
|
48
|
+
self.yml_path.touch()
|
49
|
+
self.yml_path.write_text(
|
50
|
+
utils.model_to_yaml(
|
51
|
+
Package(name='test-problem', timeLimit=1000, memoryLimit=256)
|
52
|
+
)
|
53
|
+
)
|
54
|
+
|
55
|
+
def initialize_preset(self) -> TestingPreset:
|
56
|
+
preset = presets.get_active_preset_or_null(self.root)
|
57
|
+
if preset is None:
|
58
|
+
preset_path = self.root / '.local.rbx'
|
59
|
+
preset_path.mkdir(parents=True, exist_ok=True)
|
60
|
+
else:
|
61
|
+
preset_path = presets.get_active_preset_path(self.root)
|
62
|
+
return TestingPreset(preset_path)
|
63
|
+
|
64
|
+
def print_tree(self):
|
65
|
+
print_directory_tree(self.root)
|
66
|
+
|
67
|
+
def print_yml(self):
|
68
|
+
console.console.print(self.yml_path.read_text(), highlight=True)
|
69
|
+
|
70
|
+
def print_debug(self):
|
71
|
+
self.print_yml()
|
72
|
+
self.print_tree()
|
73
|
+
|
74
|
+
@property
|
75
|
+
def yml(self) -> Package:
|
76
|
+
if self._yml is None:
|
77
|
+
self._yml = utils.model_from_yaml(Package, self.yml_path.read_text())
|
78
|
+
return self._yml
|
79
|
+
|
80
|
+
def save(self):
|
81
|
+
self.yml_path.write_text(utils.model_to_yaml(self.yml))
|
82
|
+
|
83
|
+
def set_type(self, type: TaskType):
|
84
|
+
self.yml.type = type
|
85
|
+
self.save()
|
86
|
+
|
87
|
+
def add_solution(
|
88
|
+
self,
|
89
|
+
path: PathOrStr,
|
90
|
+
outcome: ExpectedOutcome,
|
91
|
+
language: Optional[str] = None,
|
92
|
+
):
|
93
|
+
self.yml.solutions = self.yml.solutions + [
|
94
|
+
Solution(path=pathlib.Path(path), language=language, outcome=outcome)
|
95
|
+
]
|
96
|
+
self.save()
|
97
|
+
return self.add_file(path)
|
98
|
+
|
99
|
+
def add_generator(
|
100
|
+
self,
|
101
|
+
path: PathOrStr,
|
102
|
+
language: Optional[str] = None,
|
103
|
+
alias: Optional[str] = None,
|
104
|
+
src: Optional[PathOrStr] = None,
|
105
|
+
):
|
106
|
+
if alias is not None:
|
107
|
+
self.yml.generators = self.yml.generators + [
|
108
|
+
Generator(path=pathlib.Path(path), language=language, name=alias)
|
109
|
+
]
|
110
|
+
self.save()
|
111
|
+
return self.add_file(path, src=src)
|
112
|
+
|
113
|
+
def set_validator(
|
114
|
+
self,
|
115
|
+
path: PathOrStr,
|
116
|
+
language: Optional[str] = None,
|
117
|
+
src: Optional[PathOrStr] = None,
|
118
|
+
):
|
119
|
+
self.yml.validator = CodeItem(path=pathlib.Path(path), language=language)
|
120
|
+
self.save()
|
121
|
+
return self.add_file(path, src=src)
|
122
|
+
|
123
|
+
def set_checker(
|
124
|
+
self,
|
125
|
+
path: PathOrStr,
|
126
|
+
language: Optional[str] = None,
|
127
|
+
src: Optional[PathOrStr] = None,
|
128
|
+
):
|
129
|
+
self.yml.checker = CodeItem(path=pathlib.Path(path), language=language)
|
130
|
+
self.save()
|
131
|
+
return self.add_file(path, src=src)
|
132
|
+
|
133
|
+
def set_interactor(
|
134
|
+
self,
|
135
|
+
path: PathOrStr,
|
136
|
+
language: Optional[str] = None,
|
137
|
+
src: Optional[PathOrStr] = None,
|
138
|
+
):
|
139
|
+
self.yml.interactor = Interactor(path=pathlib.Path(path), language=language)
|
140
|
+
self.save()
|
141
|
+
return self.add_file(path, src=src)
|
142
|
+
|
143
|
+
def set_var(self, name: str, value: Primitive):
|
144
|
+
self.yml.vars[name] = value
|
145
|
+
self.save()
|
146
|
+
|
147
|
+
def set_vars(self, vars: Dict[str, Primitive]):
|
148
|
+
self.yml.vars = vars
|
149
|
+
self.save()
|
150
|
+
|
151
|
+
def add_testplan(self, name: str, src: Optional[PathOrStr] = None):
|
152
|
+
path = self.add_file(pathlib.Path('testplan') / f'{name}.txt', src)
|
153
|
+
return path
|
154
|
+
|
155
|
+
def add_testscript(self, name: str, src: Optional[PathOrStr] = None):
|
156
|
+
path = self.add_file(pathlib.Path('testplan') / f'{name}.py', src)
|
157
|
+
return path
|
158
|
+
|
159
|
+
def add_testgroup_from_glob(
|
160
|
+
self,
|
161
|
+
name: str,
|
162
|
+
glob: str,
|
163
|
+
validator: Optional[PathOrStr] = None,
|
164
|
+
extra_validators: Optional[List[PathOrStr]] = None,
|
165
|
+
):
|
166
|
+
self.yml.testcases = self.yml.testcases + [
|
167
|
+
TestcaseGroup(
|
168
|
+
name=name,
|
169
|
+
testcaseGlob=glob,
|
170
|
+
validator=CodeItem(path=pathlib.Path(validator)) if validator else None,
|
171
|
+
extraValidators=[
|
172
|
+
CodeItem(path=pathlib.Path(v)) for v in extra_validators
|
173
|
+
]
|
174
|
+
if extra_validators
|
175
|
+
else [],
|
176
|
+
)
|
177
|
+
]
|
178
|
+
self.save()
|
179
|
+
|
180
|
+
def add_testgroup_from_plan(
|
181
|
+
self,
|
182
|
+
name: str,
|
183
|
+
plan: str,
|
184
|
+
validator: Optional[PathOrStr] = None,
|
185
|
+
extra_validators: Optional[List[PathOrStr]] = None,
|
186
|
+
):
|
187
|
+
plan_path = self.add_testplan(name)
|
188
|
+
plan_path.write_text(plan)
|
189
|
+
self.yml.testcases = self.yml.testcases + [
|
190
|
+
TestcaseGroup(
|
191
|
+
name=name,
|
192
|
+
generatorScript=CodeItem(path=plan_path),
|
193
|
+
validator=CodeItem(path=pathlib.Path(validator)) if validator else None,
|
194
|
+
extraValidators=[
|
195
|
+
CodeItem(path=pathlib.Path(v)) for v in extra_validators
|
196
|
+
]
|
197
|
+
if extra_validators
|
198
|
+
else [],
|
199
|
+
)
|
200
|
+
]
|
201
|
+
self.save()
|
202
|
+
|
203
|
+
def add_testgroup_from_script(
|
204
|
+
self,
|
205
|
+
name: str,
|
206
|
+
script: str,
|
207
|
+
validator: Optional[PathOrStr] = None,
|
208
|
+
extra_validators: Optional[List[PathOrStr]] = None,
|
209
|
+
):
|
210
|
+
script_path = self.add_testscript(name)
|
211
|
+
script_path.write_text(script)
|
212
|
+
self.yml.testcases = self.yml.testcases + [
|
213
|
+
TestcaseGroup(
|
214
|
+
name=name,
|
215
|
+
generatorScript=CodeItem(path=script_path),
|
216
|
+
validator=CodeItem(path=pathlib.Path(validator)) if validator else None,
|
217
|
+
extraValidators=[
|
218
|
+
CodeItem(path=pathlib.Path(v)) for v in extra_validators
|
219
|
+
]
|
220
|
+
if extra_validators
|
221
|
+
else [],
|
222
|
+
)
|
223
|
+
]
|
224
|
+
self.save()
|
225
|
+
|
226
|
+
def get_build_testgroup_path(self, name: str) -> pathlib.Path:
|
227
|
+
return self.root / 'build' / 'tests' / name
|
228
|
+
|
229
|
+
def get_testcase_contents(self, path: pathlib.Path) -> TestcaseArtifacts:
|
230
|
+
contents = TestcaseArtifacts()
|
231
|
+
output_path = path.with_suffix('.out')
|
232
|
+
if output_path.exists():
|
233
|
+
contents.output = output_path.read_text()
|
234
|
+
log_path = path.with_suffix('.log')
|
235
|
+
if log_path.exists():
|
236
|
+
contents.log = Evaluation.model_validate_json(log_path.read_text())
|
237
|
+
interactor_input_path = path.with_suffix('.pin')
|
238
|
+
if interactor_input_path.exists():
|
239
|
+
contents.interactor_input = interactor_input_path.read_text()
|
240
|
+
interactor_output_path = path.with_suffix('.pout')
|
241
|
+
if interactor_output_path.exists():
|
242
|
+
contents.interactor_output = interactor_output_path.read_text()
|
243
|
+
interactor_pipes_path = path.with_suffix('.pio')
|
244
|
+
if interactor_pipes_path.exists():
|
245
|
+
contents.interactor_pipes = interactor_pipes_path.read_text()
|
246
|
+
return contents
|