rbx.cp 0.5.12__py3-none-any.whl → 0.5.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbx/box/builder.py +43 -13
- rbx/box/cd.py +1 -2
- rbx/box/checkers.py +25 -0
- rbx/box/compile.py +17 -23
- rbx/box/contest/contest_package.py +7 -1
- rbx/box/contest/main.py +26 -0
- rbx/box/contest/statements.py +8 -1
- rbx/box/environment.py +12 -6
- rbx/box/generators.py +10 -6
- rbx/box/main.py +96 -20
- rbx/box/package.py +7 -1
- rbx/box/presets/__init__.py +28 -3
- rbx/box/solutions.py +37 -11
- rbx/box/statements/build_statements.py +9 -1
- rbx/box/stresses.py +4 -3
- rbx/box/stressing/finder_parser.py +2 -2
- rbx/box/validators.py +42 -1
- rbx/clone.py +1 -1
- rbx/grading/judge/digester.py +6 -0
- rbx/grading/steps.py +70 -8
- {rbx_cp-0.5.12.dist-info → rbx_cp-0.5.14.dist-info}/METADATA +4 -4
- {rbx_cp-0.5.12.dist-info → rbx_cp-0.5.14.dist-info}/RECORD +25 -25
- {rbx_cp-0.5.12.dist-info → rbx_cp-0.5.14.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.12.dist-info → rbx_cp-0.5.14.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.12.dist-info → rbx_cp-0.5.14.dist-info}/entry_points.txt +0 -0
rbx/box/builder.py
CHANGED
@@ -9,14 +9,23 @@ from rbx.box.solutions import (
|
|
9
9
|
print_run_report,
|
10
10
|
run_solutions,
|
11
11
|
)
|
12
|
-
from rbx.box.validators import
|
12
|
+
from rbx.box.validators import (
|
13
|
+
has_validation_errors,
|
14
|
+
print_validation_report,
|
15
|
+
validate_testcases,
|
16
|
+
)
|
13
17
|
|
14
18
|
|
15
19
|
def build(
|
16
20
|
verification: environment.VerificationParam,
|
17
21
|
groups: Optional[Set[str]] = None,
|
18
|
-
output: bool = True,
|
19
|
-
) ->
|
22
|
+
output: Optional[bool] = True,
|
23
|
+
) -> bool:
|
24
|
+
no_main_solution_report = False
|
25
|
+
if output is None:
|
26
|
+
output = package.get_main_solution() is not None
|
27
|
+
no_main_solution_report = not output
|
28
|
+
|
20
29
|
with utils.StatusProgress(
|
21
30
|
'Building testcases...',
|
22
31
|
'Built [item]{processed}[/item] testcases...',
|
@@ -24,6 +33,28 @@ def build(
|
|
24
33
|
) as s:
|
25
34
|
generate_testcases(s, groups=groups)
|
26
35
|
|
36
|
+
if verification > 0:
|
37
|
+
validator = package.get_validator_or_nil()
|
38
|
+
if validator is None:
|
39
|
+
console.console.print(
|
40
|
+
'[warning]No validator found, skipping validation.[/warning]'
|
41
|
+
)
|
42
|
+
|
43
|
+
if validator is not None:
|
44
|
+
with utils.StatusProgress(
|
45
|
+
'Validating testcases...',
|
46
|
+
'Validated [item]{processed}[/item] testcases...',
|
47
|
+
keep=True,
|
48
|
+
) as s:
|
49
|
+
infos = validate_testcases(s, groups=groups)
|
50
|
+
print_validation_report(infos)
|
51
|
+
|
52
|
+
if has_validation_errors(infos):
|
53
|
+
console.console.print(
|
54
|
+
'[error]Validation failed, check the report above.[/error]'
|
55
|
+
)
|
56
|
+
return False
|
57
|
+
|
27
58
|
with utils.StatusProgress(
|
28
59
|
'Building outputs for testcases...',
|
29
60
|
'Built [item]{processed}[/item] outputs...',
|
@@ -32,23 +63,22 @@ def build(
|
|
32
63
|
if output:
|
33
64
|
generate_outputs_for_testcases(s, groups=groups)
|
34
65
|
|
35
|
-
if verification > 0:
|
36
|
-
with utils.StatusProgress(
|
37
|
-
'Validating testcases...',
|
38
|
-
'Validated [item]{processed}[/item] testcases...',
|
39
|
-
keep=True,
|
40
|
-
) as s:
|
41
|
-
infos = validate_testcases(s, groups=groups)
|
42
|
-
print_validation_report(infos)
|
43
|
-
|
44
66
|
console.console.print(
|
45
67
|
'[success]Problem built.[/success] '
|
46
68
|
'[warning]Check the output for verification errors![/warning]'
|
47
69
|
)
|
48
70
|
|
71
|
+
if no_main_solution_report:
|
72
|
+
console.console.print(
|
73
|
+
'[warning]No main solution found, skipping generating samples for the statement.[/warning]'
|
74
|
+
)
|
75
|
+
|
76
|
+
return True
|
77
|
+
|
49
78
|
|
50
79
|
def verify(verification: environment.VerificationParam) -> bool:
|
51
|
-
build(verification=verification)
|
80
|
+
if not build(verification=verification):
|
81
|
+
return False
|
52
82
|
|
53
83
|
if verification < VerificationLevel.FAST_SOLUTIONS.value:
|
54
84
|
return True
|
rbx/box/cd.py
CHANGED
rbx/box/checkers.py
CHANGED
@@ -119,6 +119,31 @@ def check(
|
|
119
119
|
)
|
120
120
|
message = package.get_digest_as_string(error.value or '') or ''
|
121
121
|
|
122
|
+
if (
|
123
|
+
checker_run_log is not None
|
124
|
+
and checker_run_log.exitcode != 0
|
125
|
+
and (
|
126
|
+
checker_run_log.exitstatus != SandboxBase.EXIT_NONZERO_RETURN
|
127
|
+
or checker_run_log.exitcode not in [0, 1, 2, 3]
|
128
|
+
)
|
129
|
+
):
|
130
|
+
console.console.print(
|
131
|
+
f'[error]Checker [item]{package.get_checker().path}[/item] failed unexpectedly.[/error]'
|
132
|
+
)
|
133
|
+
console.console.print(
|
134
|
+
f'[error]Summary:[/error] {checker_run_log.get_summary()}'
|
135
|
+
)
|
136
|
+
console.console.print(
|
137
|
+
f'[error]Testcase input:[/error] [item]{testcase.inputPath}[/item]'
|
138
|
+
)
|
139
|
+
console.console.print(
|
140
|
+
f'[error]Testcase output:[/error] [item]{testcase.outputPath}[/item]'
|
141
|
+
)
|
142
|
+
console.console.print(
|
143
|
+
f'[error]Program output:[/error] [item]{program_output}[/item]'
|
144
|
+
)
|
145
|
+
raise typer.Exit(1)
|
146
|
+
|
122
147
|
if checker_run_log is None or checker_run_log.exitcode not in [0, 1, 2, 3]:
|
123
148
|
return CheckerResult(outcome=Outcome.INTERNAL_ERROR)
|
124
149
|
|
rbx/box/compile.py
CHANGED
@@ -22,35 +22,29 @@ def _compile(item: CodeItem):
|
|
22
22
|
out_path.chmod(0o755)
|
23
23
|
|
24
24
|
console.console.print(
|
25
|
-
f'[success]Compiled file written at [item]{out_path}[/item]
|
25
|
+
f'[success]Compiled file written at [item]{out_path}[/item][/success]'
|
26
26
|
)
|
27
27
|
|
28
28
|
|
29
|
-
@app.command('any, a', help='Compile an asset given its path.')
|
30
|
-
@package.within_problem
|
31
29
|
def any(path: str):
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
@app.command('solution, s', help='Compile a solution given its path.')
|
36
|
-
@package.within_problem
|
37
|
-
def solution(path: str):
|
38
|
-
_compile(package.get_solution(path))
|
30
|
+
pkg = package.find_problem_package_or_die()
|
39
31
|
|
32
|
+
solution = package.get_solution_or_nil(path)
|
33
|
+
if solution is not None:
|
34
|
+
_compile(solution)
|
35
|
+
return
|
40
36
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
37
|
+
for generator in pkg.generators:
|
38
|
+
if generator.path == pathlib.Path(path) or generator.name == path:
|
39
|
+
_compile(generator)
|
40
|
+
return
|
45
41
|
|
42
|
+
if pkg.checker is not None and pkg.checker.path == pathlib.Path(path):
|
43
|
+
_compile(pkg.checker)
|
44
|
+
return
|
46
45
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
_compile(package.get_checker())
|
46
|
+
if pkg.validator is not None and pkg.validator.path == pathlib.Path(path):
|
47
|
+
_compile(pkg.validator)
|
48
|
+
return
|
51
49
|
|
52
|
-
|
53
|
-
@app.command('validator, v', help='Compile the main validator.')
|
54
|
-
@package.within_problem
|
55
|
-
def validator():
|
56
|
-
_compile(package.get_validator())
|
50
|
+
_compile(CodeItem(path=pathlib.Path(path)))
|
@@ -3,6 +3,7 @@ import pathlib
|
|
3
3
|
from typing import List, Optional
|
4
4
|
|
5
5
|
import typer
|
6
|
+
from pydantic import ValidationError
|
6
7
|
|
7
8
|
from rbx import console, utils
|
8
9
|
from rbx.box.contest.schema import Contest
|
@@ -30,7 +31,12 @@ def find_contest_package(root: pathlib.Path = pathlib.Path()) -> Optional[Contes
|
|
30
31
|
contest_yaml_path = find_contest_yaml(root)
|
31
32
|
if not contest_yaml_path:
|
32
33
|
return None
|
33
|
-
|
34
|
+
try:
|
35
|
+
return utils.model_from_yaml(Contest, contest_yaml_path.read_text())
|
36
|
+
except ValidationError as e:
|
37
|
+
console.console.print(e)
|
38
|
+
console.console.print('[error]Error parsing contest.rbx.yml.[/error]')
|
39
|
+
raise typer.Exit(1) from e
|
34
40
|
|
35
41
|
|
36
42
|
def find_contest_package_or_die(root: pathlib.Path = pathlib.Path()) -> Contest:
|
rbx/box/contest/main.py
CHANGED
@@ -160,6 +160,32 @@ def add(path: str, short_name: str, preset: Optional[str] = None):
|
|
160
160
|
)
|
161
161
|
|
162
162
|
|
163
|
+
@app.command('remove, r', help='Remove problem from contest.')
|
164
|
+
@within_contest
|
165
|
+
def remove(path_or_short_name: str):
|
166
|
+
contest = find_contest_package_or_die()
|
167
|
+
|
168
|
+
kept_problems = []
|
169
|
+
removed_problems = []
|
170
|
+
for problem in contest.problems:
|
171
|
+
if (
|
172
|
+
problem.path == pathlib.Path(path_or_short_name)
|
173
|
+
or problem.short_name == path_or_short_name
|
174
|
+
):
|
175
|
+
removed_problems.append(problem)
|
176
|
+
else:
|
177
|
+
kept_problems.append(problem)
|
178
|
+
|
179
|
+
contest.problems = kept_problems
|
180
|
+
save_contest(contest)
|
181
|
+
|
182
|
+
for problem in removed_problems:
|
183
|
+
shutil.rmtree(str(problem.path), ignore_errors=True)
|
184
|
+
console.console.print(
|
185
|
+
f'Problem [item]{problem.short_name}[/item] removed from contest at [item]{problem.path}[/item].'
|
186
|
+
)
|
187
|
+
|
188
|
+
|
163
189
|
@app.command(
|
164
190
|
'each',
|
165
191
|
help='Run a command for each problem in the contest.',
|
rbx/box/contest/statements.py
CHANGED
@@ -51,7 +51,14 @@ def build(
|
|
51
51
|
)
|
52
52
|
with utils.new_cd(problem.get_path()):
|
53
53
|
contest_utils.clear_package_cache()
|
54
|
-
|
54
|
+
|
55
|
+
if not builder.build(
|
56
|
+
verification=verification, groups=set(['samples']), output=None
|
57
|
+
):
|
58
|
+
console.console.print(
|
59
|
+
'[error]Failed to build statements with samples, aborting.[/error]'
|
60
|
+
)
|
61
|
+
raise typer.Exit(1)
|
55
62
|
|
56
63
|
contest = find_contest_package_or_die()
|
57
64
|
candidate_languages = languages
|
rbx/box/environment.py
CHANGED
@@ -4,7 +4,7 @@ from enum import Enum
|
|
4
4
|
from typing import Annotated, List, Optional, Type, TypeVar
|
5
5
|
|
6
6
|
import typer
|
7
|
-
from pydantic import BaseModel, ConfigDict
|
7
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
8
8
|
|
9
9
|
from rbx import config, console, utils
|
10
10
|
from rbx.box.extensions import Extensions, LanguageExtensions
|
@@ -19,9 +19,8 @@ class VerificationLevel(Enum):
|
|
19
19
|
NONE = 0
|
20
20
|
VALIDATE = 1
|
21
21
|
FAST_SOLUTIONS = 2
|
22
|
-
|
23
|
-
|
24
|
-
FULL = 5
|
22
|
+
ALL_SOLUTIONS = 3
|
23
|
+
FULL = 4
|
25
24
|
|
26
25
|
|
27
26
|
VerificationParam = Annotated[
|
@@ -31,7 +30,7 @@ VerificationParam = Annotated[
|
|
31
30
|
'--verification',
|
32
31
|
'-v',
|
33
32
|
help='Verification level to use when building package.',
|
34
|
-
default_factory=lambda: VerificationLevel.
|
33
|
+
default_factory=lambda: VerificationLevel.FULL.value,
|
35
34
|
),
|
36
35
|
]
|
37
36
|
|
@@ -193,7 +192,14 @@ def get_environment(env: Optional[str] = None) -> Environment:
|
|
193
192
|
f'Environment file [item]{env_path}[/item] not found.', style='error'
|
194
193
|
)
|
195
194
|
raise typer.Exit()
|
196
|
-
|
195
|
+
try:
|
196
|
+
return utils.model_from_yaml(Environment, env_path.read_text())
|
197
|
+
except ValidationError as e:
|
198
|
+
console.console.print(e)
|
199
|
+
console.console.print(
|
200
|
+
f'[error]Error parsing environment file [item]{env_path}[/item][/error]'
|
201
|
+
)
|
202
|
+
raise typer.Exit(1) from e
|
197
203
|
|
198
204
|
|
199
205
|
@functools.cache
|
rbx/box/generators.py
CHANGED
@@ -82,6 +82,8 @@ def _run_generator(
|
|
82
82
|
console.console.print(
|
83
83
|
f'[error]Failed generating test {i} from group path {group_path}[/error]',
|
84
84
|
)
|
85
|
+
if run_log is not None:
|
86
|
+
console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
|
85
87
|
if generation_stderr.value is not None:
|
86
88
|
console.console.print('[error]Stderr:[/error]')
|
87
89
|
console.console.print(
|
@@ -142,9 +144,7 @@ def generate_output_for_testcase(
|
|
142
144
|
f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
|
143
145
|
)
|
144
146
|
if run_log is not None:
|
145
|
-
console.console.print(
|
146
|
-
f'[error]Main solution exited with code [item]{-run_log.exitcode}[/item][/error]',
|
147
|
-
)
|
147
|
+
console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
|
148
148
|
checker_result = checkers.check_with_no_output(run_log)
|
149
149
|
console.console.print(
|
150
150
|
f'[warning]Time: [item]{run_log.time:.2f}s[/item][/warning]',
|
@@ -238,7 +238,7 @@ def _run_generator_script(testcase: TestcaseSubgroup, cacher: FileCacher) -> str
|
|
238
238
|
)
|
239
239
|
if run_log is not None:
|
240
240
|
console.console.print(
|
241
|
-
f'[error]
|
241
|
+
f'[error]Summary:[/error] {run_log.get_summary()}'
|
242
242
|
)
|
243
243
|
if run_stderr.value is not None:
|
244
244
|
console.console.print('[error]Stderr:[/error]')
|
@@ -339,8 +339,12 @@ def generate_standalone(
|
|
339
339
|
)
|
340
340
|
if not generation_log or generation_log.exitcode != 0:
|
341
341
|
console.console.print(
|
342
|
-
f'[error]Failed generating test using generator call [info]{call.name} {expanded_args_str}[/info]
|
342
|
+
f'[error]Failed generating test using generator call [info]{call.name} {expanded_args_str}[/info][/error]',
|
343
343
|
)
|
344
|
+
if generation_log is not None:
|
345
|
+
console.console.print(
|
346
|
+
f'[error]Summary:[/error] {generation_log.get_summary()}'
|
347
|
+
)
|
344
348
|
if generation_stderr.value is not None:
|
345
349
|
console.console.print('[error]Stderr:[/error]')
|
346
350
|
console.console.print(
|
@@ -357,7 +361,7 @@ def generate_standalone(
|
|
357
361
|
ok, message, *_ = validators.validate_test(output, validator, validator_digest)
|
358
362
|
if not ok:
|
359
363
|
console.console.print(
|
360
|
-
f'[error]Failed validating testcase generated by call [info]{call.name} {expanded_args_str}[/info]
|
364
|
+
f'[error]Failed validating testcase generated by call [info]{call.name} {expanded_args_str}[/info][/error]'
|
361
365
|
)
|
362
366
|
console.console.print(f'[error]Message:[/error] {message}')
|
363
367
|
console.console.print(f'Testcase written at [item]{output}[/item]')
|
rbx/box/main.py
CHANGED
@@ -3,11 +3,12 @@ from gevent import monkey
|
|
3
3
|
|
4
4
|
monkey.patch_all()
|
5
5
|
|
6
|
+
import tempfile
|
6
7
|
import shlex
|
7
8
|
import sys
|
8
9
|
import typing
|
9
10
|
|
10
|
-
from rbx.box.schema import CodeItem, ExpectedOutcome
|
11
|
+
from rbx.box.schema import CodeItem, ExpectedOutcome, TestcaseGroup
|
11
12
|
|
12
13
|
|
13
14
|
import pathlib
|
@@ -31,6 +32,7 @@ from rbx.box import (
|
|
31
32
|
compile,
|
32
33
|
presets,
|
33
34
|
stresses,
|
35
|
+
validators,
|
34
36
|
)
|
35
37
|
from rbx.box.contest import main as contest
|
36
38
|
from rbx.box.environment import VerificationLevel, get_environment_path
|
@@ -42,7 +44,6 @@ from rbx.box.solutions import (
|
|
42
44
|
run_solutions,
|
43
45
|
)
|
44
46
|
from rbx.box.statements import build_statements
|
45
|
-
from rbx.box.ui import main as ui_pkg
|
46
47
|
|
47
48
|
app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
48
49
|
app.add_typer(
|
@@ -69,15 +70,12 @@ app.add_typer(
|
|
69
70
|
app.add_typer(
|
70
71
|
contest.app, name='contest', cls=annotations.AliasGroup, help='Contest management.'
|
71
72
|
)
|
72
|
-
app.add_typer(
|
73
|
-
compile.app, name='compile', cls=annotations.AliasGroup, help='Compile assets.'
|
74
|
-
)
|
75
73
|
|
76
74
|
|
77
|
-
@app.command('ui', hidden=True)
|
78
|
-
@package.within_problem
|
79
|
-
def ui():
|
80
|
-
|
75
|
+
# @app.command('ui', hidden=True)
|
76
|
+
# @package.within_problem
|
77
|
+
# def ui():
|
78
|
+
# ui_pkg.start()
|
81
79
|
|
82
80
|
|
83
81
|
@app.command('edit, e', help='Open problem.rbx.yml in your default editor.')
|
@@ -96,13 +94,6 @@ def build(verification: environment.VerificationParam):
|
|
96
94
|
builder.build(verification=verification)
|
97
95
|
|
98
96
|
|
99
|
-
@app.command('verify, v', help='Build and verify all the tests for the problem.')
|
100
|
-
@package.within_problem
|
101
|
-
def verify(verification: environment.VerificationParam):
|
102
|
-
if not builder.verify(verification=verification):
|
103
|
-
console.console.print('[error]Verification failed, check the report.[/error]')
|
104
|
-
|
105
|
-
|
106
97
|
@app.command('run, r', help='Build and run solution(s).')
|
107
98
|
@package.within_problem
|
108
99
|
def run(
|
@@ -139,7 +130,14 @@ def run(
|
|
139
130
|
)
|
140
131
|
check = False
|
141
132
|
|
142
|
-
builder.build(verification=verification, output=check)
|
133
|
+
if not builder.build(verification=verification, output=check):
|
134
|
+
return
|
135
|
+
|
136
|
+
if verification <= VerificationLevel.VALIDATE.value:
|
137
|
+
console.console.print(
|
138
|
+
'[warning]Verification level is set to [item]validate (-v1)[/item], so rbx only build tests and validated them.[/warning]'
|
139
|
+
)
|
140
|
+
return
|
143
141
|
|
144
142
|
with utils.StatusProgress('Running solutions...') as s:
|
145
143
|
tracked_solutions = None
|
@@ -206,6 +204,12 @@ def irun(
|
|
206
204
|
console.console.print(
|
207
205
|
'[warning]Outputs will be written to files. If you wish to print them to the terminal, use the "-p" parameter.'
|
208
206
|
)
|
207
|
+
if verification < VerificationLevel.ALL_SOLUTIONS.value:
|
208
|
+
console.console.print(
|
209
|
+
'[warning]Verification level should be at least [item]all solutions (-v4)[/item] to run solutions interactively.'
|
210
|
+
)
|
211
|
+
return
|
212
|
+
|
209
213
|
main_solution = package.get_main_solution()
|
210
214
|
if check and main_solution is None:
|
211
215
|
console.console.print(
|
@@ -248,7 +252,13 @@ def create(
|
|
248
252
|
@app.command('stress', help='Run a stress test.')
|
249
253
|
@package.within_problem
|
250
254
|
def stress(
|
251
|
-
name:
|
255
|
+
name: Annotated[
|
256
|
+
str,
|
257
|
+
typer.Argument(
|
258
|
+
help='Name of the stress test to run (specified in problem.rbx.yml), '
|
259
|
+
'or the generator to run, in case -g is specified.'
|
260
|
+
),
|
261
|
+
],
|
252
262
|
generator_args: Annotated[
|
253
263
|
Optional[str],
|
254
264
|
typer.Option(
|
@@ -325,9 +335,27 @@ def stress(
|
|
325
335
|
|
326
336
|
testgroup = questionary.select(
|
327
337
|
'Choose the testgroup to add the tests to.\nOnly test groups that have a .txt generatorScript are shown below: ',
|
328
|
-
choices=list(groups_by_name) + ['(skip)'],
|
338
|
+
choices=list(groups_by_name) + ['(create new script)', '(skip)'],
|
329
339
|
).ask()
|
330
340
|
|
341
|
+
if testgroup == '(create new script)':
|
342
|
+
new_script_name = questionary.text(
|
343
|
+
'Enter the name of the new .txt generatorScript file: '
|
344
|
+
).ask()
|
345
|
+
new_script_path = pathlib.Path(new_script_name).with_suffix('.txt')
|
346
|
+
new_script_path.parent.mkdir(parents=True, exist_ok=True)
|
347
|
+
new_script_path.touch()
|
348
|
+
|
349
|
+
# Temporarily create a new testgroup with the new script.
|
350
|
+
testgroup = new_script_path.stem
|
351
|
+
groups_by_name[testgroup] = TestcaseGroup(
|
352
|
+
name=testgroup, generatorScript=CodeItem(path=new_script_path)
|
353
|
+
)
|
354
|
+
console.console.print(
|
355
|
+
f'[warning]A testgroup for [item]{new_script_path}[/item] will not be automatically added to the problem.rbx.yml file for you.\n'
|
356
|
+
'Please add it manually. [/warning]'
|
357
|
+
)
|
358
|
+
|
331
359
|
if testgroup not in groups_by_name:
|
332
360
|
break
|
333
361
|
try:
|
@@ -355,6 +383,47 @@ def stress(
|
|
355
383
|
break
|
356
384
|
|
357
385
|
|
386
|
+
@app.command('compile', help='Compile an asset given its path.')
|
387
|
+
@package.within_problem
|
388
|
+
def compile_command(
|
389
|
+
path: Annotated[str, typer.Argument(help='Path to the asset to compile.')],
|
390
|
+
):
|
391
|
+
compile.any(path)
|
392
|
+
|
393
|
+
|
394
|
+
@app.command('validate', help='Run the validator in a one-off fashion, interactively.')
|
395
|
+
@package.within_problem
|
396
|
+
def validate(
|
397
|
+
path: Annotated[
|
398
|
+
Optional[str],
|
399
|
+
typer.Option('--path', '-p', help='Path to the testcase to validate.'),
|
400
|
+
] = None,
|
401
|
+
):
|
402
|
+
validator_tuple = validators.compile_main_validator()
|
403
|
+
if validator_tuple is None:
|
404
|
+
console.console.print('[error]No validator found for this problem.[/error]')
|
405
|
+
raise typer.Exit(1)
|
406
|
+
|
407
|
+
validator, validator_digest = validator_tuple
|
408
|
+
|
409
|
+
input = console.multiline_prompt('Testcase input')
|
410
|
+
|
411
|
+
if path is None:
|
412
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
413
|
+
tmppath = pathlib.Path(tmpdir) / '000.in'
|
414
|
+
tmppath.write_text(input)
|
415
|
+
|
416
|
+
info = validators.validate_one_off(
|
417
|
+
pathlib.Path(tmppath), validator, validator_digest
|
418
|
+
)
|
419
|
+
else:
|
420
|
+
info = validators.validate_one_off(
|
421
|
+
pathlib.Path(path), validator, validator_digest
|
422
|
+
)
|
423
|
+
|
424
|
+
validators.print_validation_report([info])
|
425
|
+
|
426
|
+
|
358
427
|
@app.command('environment, env', help='Set or show the current box environment.')
|
359
428
|
def environment_command(
|
360
429
|
env: Annotated[Optional[str], typer.Argument()] = None,
|
@@ -417,10 +486,17 @@ def activate():
|
|
417
486
|
console.console.print(
|
418
487
|
'[error]Preset is not installed. Install it manually, or specify a URI in [item].preset-lock.yml[/item].[/error]'
|
419
488
|
)
|
420
|
-
raise
|
489
|
+
raise typer.Exit(1)
|
421
490
|
presets.install(preset_lock.uri)
|
422
491
|
|
423
492
|
preset = presets.get_installed_preset(preset_lock.preset_name)
|
493
|
+
|
494
|
+
# Install the environment from the preset if it's not already installed.
|
495
|
+
presets.optionally_install_environment_from_preset(
|
496
|
+
preset, root=presets.get_preset_installation_path(preset_lock.name)
|
497
|
+
)
|
498
|
+
|
499
|
+
# Activate the environment.
|
424
500
|
if preset.env is not None:
|
425
501
|
environment_command(preset.name)
|
426
502
|
|
rbx/box/package.py
CHANGED
@@ -3,6 +3,7 @@ import pathlib
|
|
3
3
|
from typing import Dict, List, Optional, Tuple
|
4
4
|
|
5
5
|
import typer
|
6
|
+
from pydantic import ValidationError
|
6
7
|
|
7
8
|
from rbx import config, console, utils
|
8
9
|
from rbx.box import environment
|
@@ -74,7 +75,12 @@ def find_problem_package(root: pathlib.Path = pathlib.Path()) -> Optional[Packag
|
|
74
75
|
problem_yaml_path = find_problem_yaml(root)
|
75
76
|
if not problem_yaml_path:
|
76
77
|
return None
|
77
|
-
|
78
|
+
try:
|
79
|
+
return utils.model_from_yaml(Package, problem_yaml_path.read_text())
|
80
|
+
except ValidationError as e:
|
81
|
+
console.console.print(e)
|
82
|
+
console.console.print('[error]Error parsing problem.rbx.yml.[/error]')
|
83
|
+
raise typer.Exit(1) from e
|
78
84
|
|
79
85
|
|
80
86
|
def find_problem_package_or_die(root: pathlib.Path = pathlib.Path()) -> Package:
|
rbx/box/presets/__init__.py
CHANGED
@@ -4,6 +4,7 @@ import tempfile
|
|
4
4
|
from typing import Annotated, Iterable, List, Optional, Sequence, Union
|
5
5
|
|
6
6
|
import git
|
7
|
+
import questionary
|
7
8
|
import rich
|
8
9
|
import rich.prompt
|
9
10
|
import typer
|
@@ -16,7 +17,7 @@ from rbx.box.presets.fetch import PresetFetchInfo, get_preset_fetch_info
|
|
16
17
|
from rbx.box.presets.lock_schema import LockedAsset, PresetLock
|
17
18
|
from rbx.box.presets.schema import Preset, TrackedAsset
|
18
19
|
from rbx.config import get_default_app_path
|
19
|
-
from rbx.grading.judge.digester import digest_cooperatively
|
20
|
+
from rbx.grading.judge.digester import digest_cooperatively, digest_file
|
20
21
|
|
21
22
|
app = typer.Typer(no_args_is_help=True)
|
22
23
|
|
@@ -34,7 +35,7 @@ def get_preset_yaml(root: pathlib.Path = pathlib.Path()) -> Preset:
|
|
34
35
|
found = find_preset_yaml(root)
|
35
36
|
if not found:
|
36
37
|
console.console.print(
|
37
|
-
f'[error][item]preset.rbx.yml[/item] not found in [item]{root.absolute()}[/item]
|
38
|
+
f'[error][item]preset.rbx.yml[/item] not found in [item]{root.absolute()}[/item][/error]'
|
38
39
|
)
|
39
40
|
raise typer.Exit(1)
|
40
41
|
return utils.model_from_yaml(Preset, found.read_text())
|
@@ -294,6 +295,30 @@ def get_installed_preset(name: str, root: pathlib.Path = pathlib.Path()) -> Pres
|
|
294
295
|
return preset
|
295
296
|
|
296
297
|
|
298
|
+
def optionally_install_environment_from_preset(
|
299
|
+
preset: Preset, root: pathlib.Path = pathlib.Path()
|
300
|
+
):
|
301
|
+
if preset.env is None:
|
302
|
+
return
|
303
|
+
env_path = get_environment_path(preset.name)
|
304
|
+
preset_env_path = root / preset.env
|
305
|
+
if env_path.is_file():
|
306
|
+
if digest_file(preset_env_path) == digest_file(env_path):
|
307
|
+
return
|
308
|
+
overwrite = questionary.confirm(
|
309
|
+
'Preset environment file has changed. Overwrite?',
|
310
|
+
default=False,
|
311
|
+
).ask()
|
312
|
+
if not overwrite:
|
313
|
+
return
|
314
|
+
|
315
|
+
console.console.print(
|
316
|
+
f'[success]Overwriting the existing environment based on [item]{preset.env}[/item].'
|
317
|
+
)
|
318
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
319
|
+
shutil.copyfile(str(preset_env_path), env_path)
|
320
|
+
|
321
|
+
|
297
322
|
def _install(root: pathlib.Path = pathlib.Path(), force: bool = False):
|
298
323
|
preset = get_preset_yaml(root)
|
299
324
|
|
@@ -394,7 +419,7 @@ def _sync(update: bool = False):
|
|
394
419
|
'[error]Package does not have a [item].preset.lock.yml[/item] file and thus cannot be synced.[/error]'
|
395
420
|
)
|
396
421
|
console.console.print(
|
397
|
-
'[error]Ensure this package was created through a preset, or manually associate one with [item]rbx presets lock [PRESET][/item]
|
422
|
+
'[error]Ensure this package was created through a preset, or manually associate one with [item]rbx presets lock [PRESET][/item][/error]'
|
398
423
|
)
|
399
424
|
raise typer.Exit(1)
|
400
425
|
|
rbx/box/solutions.py
CHANGED
@@ -114,7 +114,7 @@ def compile_solutions(
|
|
114
114
|
compiled_solutions[solution.path] = compile_item(solution)
|
115
115
|
except:
|
116
116
|
console.console.print(
|
117
|
-
f'[error]Failed compiling solution [item]{solution.path}[/item]
|
117
|
+
f'[error]Failed compiling solution [item]{solution.path}[/item][/error]'
|
118
118
|
)
|
119
119
|
raise
|
120
120
|
|
@@ -480,10 +480,14 @@ def get_testcase_markup_verdict(eval: Evaluation) -> str:
|
|
480
480
|
|
481
481
|
|
482
482
|
def _get_evals_time_in_ms(evals: List[Evaluation]) -> int:
|
483
|
+
if not evals:
|
484
|
+
return 0
|
483
485
|
return max(int((eval.log.time or 0.0) * 1000) for eval in evals)
|
484
486
|
|
485
487
|
|
486
488
|
def _get_evals_memory_in_mb(evals: List[Evaluation]) -> int:
|
489
|
+
if not evals:
|
490
|
+
return 0
|
487
491
|
return max(int(eval.log.memory or 0) // (1024 * 1024) for eval in evals)
|
488
492
|
|
489
493
|
|
@@ -505,6 +509,7 @@ def _print_solution_outcome(
|
|
505
509
|
) -> bool:
|
506
510
|
pkg = package.find_problem_package_or_die()
|
507
511
|
|
512
|
+
has_plain_tle = False
|
508
513
|
bad_verdicts = set()
|
509
514
|
no_tle_bad_verdicts = set()
|
510
515
|
for eval in evals:
|
@@ -515,6 +520,10 @@ def _print_solution_outcome(
|
|
515
520
|
and eval.result.no_tle_outcome != Outcome.ACCEPTED
|
516
521
|
):
|
517
522
|
no_tle_bad_verdicts.add(eval.result.no_tle_outcome)
|
523
|
+
has_plain_tle = has_plain_tle or (
|
524
|
+
eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
|
525
|
+
and eval.result.no_tle_outcome is None
|
526
|
+
)
|
518
527
|
|
519
528
|
unmatched_bad_verdicts = set(
|
520
529
|
v for v in bad_verdicts if not solution.outcome.match(v)
|
@@ -543,16 +552,27 @@ def _print_solution_outcome(
|
|
543
552
|
verification.value >= VerificationLevel.FULL.value
|
544
553
|
# Solution expects a TLE.
|
545
554
|
and expected_outcome_is_tle
|
546
|
-
#
|
547
|
-
and
|
548
|
-
#
|
549
|
-
and
|
550
|
-
# The solution
|
555
|
+
# Solution does not have a plain TLE.
|
556
|
+
and not has_plain_tle
|
557
|
+
# A TLE has happened.
|
558
|
+
and Outcome.TIME_LIMIT_EXCEEDED in matched_bad_verdicts
|
559
|
+
# The solution runs in double TL.
|
551
560
|
and evals_time < pkg.timelimit_for_language(solution.language) * 2
|
552
561
|
):
|
553
|
-
|
554
|
-
|
555
|
-
|
562
|
+
other_verdicts = (bad_verdicts | no_tle_bad_verdicts) - {
|
563
|
+
Outcome.TIME_LIMIT_EXCEEDED
|
564
|
+
}
|
565
|
+
if not other_verdicts:
|
566
|
+
# The solution has no other bad verdicts except for TLEs in double TL.
|
567
|
+
console.print(
|
568
|
+
'[yellow]WARNING[/yellow] The solution still passed in double TL.'
|
569
|
+
)
|
570
|
+
elif not (bad_verdicts - {Outcome.TIME_LIMIT_EXCEEDED}):
|
571
|
+
# The solution has other bad soft TLE outcomes.
|
572
|
+
other_verdicts_names = ' '.join(v.name for v in other_verdicts)
|
573
|
+
console.print(
|
574
|
+
f'[yellow]WARNING[/yellow] The solution could still run under double TL, but failed with [item]{other_verdicts_names}[/item].'
|
575
|
+
)
|
556
576
|
console.print(f'Time: {get_evals_formatted_time(evals)}')
|
557
577
|
console.print(f'Memory: {get_evals_formatted_memory(evals)}')
|
558
578
|
return len(unmatched_bad_verdicts) == 0
|
@@ -758,7 +778,10 @@ def print_run_report(
|
|
758
778
|
is_closing_group = last_group is not None and is_new_group
|
759
779
|
|
760
780
|
if is_closing_group:
|
761
|
-
console.print(
|
781
|
+
console.print(
|
782
|
+
f'({get_evals_formatted_time(group_evals)}, {get_evals_formatted_memory(group_evals)})',
|
783
|
+
end='',
|
784
|
+
)
|
762
785
|
console.print()
|
763
786
|
|
764
787
|
if is_new_solution:
|
@@ -788,6 +811,9 @@ def print_run_report(
|
|
788
811
|
print_last_solution()
|
789
812
|
|
790
813
|
items.seek(0)
|
791
|
-
|
814
|
+
structured_evaluations_list = list(structured_evaluations)
|
815
|
+
|
816
|
+
if structured_evaluations_list:
|
817
|
+
_print_timing(console, result.skeleton, structured_evaluations_list[-1])
|
792
818
|
|
793
819
|
return ok
|
@@ -330,7 +330,15 @@ def build(
|
|
330
330
|
):
|
331
331
|
# At most run the validators, only in samples.
|
332
332
|
if samples:
|
333
|
-
builder.build(
|
333
|
+
if not builder.build(
|
334
|
+
verification=verification,
|
335
|
+
groups=set(['samples']),
|
336
|
+
output=None,
|
337
|
+
):
|
338
|
+
console.console.print(
|
339
|
+
'[error]Failed to build statements with samples, aborting.[/error]'
|
340
|
+
)
|
341
|
+
raise typer.Exit(1)
|
334
342
|
|
335
343
|
pkg = package.find_problem_package_or_die()
|
336
344
|
candidate_languages = languages
|
rbx/box/stresses.py
CHANGED
@@ -35,7 +35,7 @@ def _compile_finder(finder: CodeItem) -> str:
|
|
35
35
|
digest = compile_item(finder)
|
36
36
|
except Exception as e:
|
37
37
|
console.console.print(
|
38
|
-
f'[error]Failed compiling checker [item]{finder.path}[/item]
|
38
|
+
f'[error]Failed compiling checker [item]{finder.path}[/item][/error]'
|
39
39
|
)
|
40
40
|
raise typer.Exit(1) from e
|
41
41
|
return digest
|
@@ -104,6 +104,8 @@ def run_stress(
|
|
104
104
|
if time.monotonic() - startTime > timeoutInSeconds:
|
105
105
|
break
|
106
106
|
|
107
|
+
executed += 1
|
108
|
+
|
107
109
|
if progress:
|
108
110
|
seconds = timeoutInSeconds - int(time.monotonic() - startTime)
|
109
111
|
progress.update(
|
@@ -212,7 +214,7 @@ def run_stress(
|
|
212
214
|
|
213
215
|
if internal_error_results:
|
214
216
|
console.console.print(
|
215
|
-
f'[error]Checkers failed during stress test [item]{name}[/item] with args [info]{expanded_generator_call.name} {expanded_generator_call.args}[/info]
|
217
|
+
f'[error]Checkers failed during stress test [item]{name}[/item] with args [info]{expanded_generator_call.name} {expanded_generator_call.args}[/info][/error]'
|
216
218
|
)
|
217
219
|
for internal_error_result in internal_error_results:
|
218
220
|
assert internal_error_result.checker is not None
|
@@ -259,7 +261,6 @@ def run_stress(
|
|
259
261
|
)
|
260
262
|
|
261
263
|
# Be cooperative.
|
262
|
-
executed += 1
|
263
264
|
time.sleep(0.001)
|
264
265
|
|
265
266
|
return StressReport(findings=findings, executed=executed)
|
@@ -300,7 +300,7 @@ def validate(tree: lark.ParseTree):
|
|
300
300
|
for checker in all_checkers:
|
301
301
|
if not pathlib.Path(checker).is_file():
|
302
302
|
console.console.print(
|
303
|
-
f'[error]Finder expression references non-existing checker [item]{checker}[/item]
|
303
|
+
f'[error]Finder expression references non-existing checker [item]{checker}[/item][/error]'
|
304
304
|
)
|
305
305
|
raise typer.Exit(1)
|
306
306
|
|
@@ -308,7 +308,7 @@ def validate(tree: lark.ParseTree):
|
|
308
308
|
for solution in all_solutions:
|
309
309
|
if not pathlib.Path(solution).is_file():
|
310
310
|
console.console.print(
|
311
|
-
f'[error]Finder expression references non-existing solution [item]{solution}[/item]
|
311
|
+
f'[error]Finder expression references non-existing solution [item]{solution}[/item][/error]'
|
312
312
|
)
|
313
313
|
raise typer.Exit(1)
|
314
314
|
|
rbx/box/validators.py
CHANGED
@@ -2,6 +2,7 @@ import pathlib
|
|
2
2
|
import shlex
|
3
3
|
from typing import Dict, List, Optional, Set, Tuple
|
4
4
|
|
5
|
+
import typer
|
5
6
|
from pydantic import BaseModel
|
6
7
|
|
7
8
|
from rbx import console
|
@@ -9,6 +10,7 @@ from rbx.box import package
|
|
9
10
|
from rbx.box.code import compile_item, run_item
|
10
11
|
from rbx.box.schema import CodeItem, Primitive
|
11
12
|
from rbx.box.testcases import find_built_testcase_inputs
|
13
|
+
from rbx.grading.judge.sandbox import SandboxBase
|
12
14
|
from rbx.grading.steps import (
|
13
15
|
DigestHolder,
|
14
16
|
DigestOrDest,
|
@@ -33,7 +35,7 @@ def _compile_validator(validator: CodeItem) -> str:
|
|
33
35
|
digest = compile_item(validator)
|
34
36
|
except:
|
35
37
|
console.console.print(
|
36
|
-
f'[error]Failed compiling validator [item]{validator.path}[/item]
|
38
|
+
f'[error]Failed compiling validator [item]{validator.path}[/item][/error]'
|
37
39
|
)
|
38
40
|
raise
|
39
41
|
return digest
|
@@ -52,6 +54,9 @@ def _process_bounds(log: str) -> HitBounds:
|
|
52
54
|
k, v = items
|
53
55
|
v = v.strip()
|
54
56
|
|
57
|
+
if 'constant-bounds' in k:
|
58
|
+
continue
|
59
|
+
|
55
60
|
hit = ('min-value-hit' in v, 'max-value-hit' in v)
|
56
61
|
if k not in bounds:
|
57
62
|
bounds[k] = hit
|
@@ -108,6 +113,18 @@ def _validate_testcase(
|
|
108
113
|
],
|
109
114
|
extra_args=shlex.join(var_args) if var_args else None,
|
110
115
|
)
|
116
|
+
|
117
|
+
if (
|
118
|
+
run_log is not None
|
119
|
+
and run_log.exitcode != 0
|
120
|
+
and run_log.exitstatus != SandboxBase.EXIT_NONZERO_RETURN
|
121
|
+
):
|
122
|
+
console.console.print(
|
123
|
+
f'[error]Validator [item]{validator.path}[/item] failed unexpectedly.[/error]'
|
124
|
+
)
|
125
|
+
console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
|
126
|
+
raise typer.Exit(1)
|
127
|
+
|
111
128
|
log_overview = ''
|
112
129
|
if log_digest.value is not None:
|
113
130
|
log_overview = package.get_digest_as_string(log_digest.value or '')
|
@@ -138,6 +155,22 @@ def compile_main_validator() -> Optional[Tuple[CodeItem, str]]:
|
|
138
155
|
return pkg.validator, _compile_validator(pkg.validator)
|
139
156
|
|
140
157
|
|
158
|
+
def validate_one_off(
|
159
|
+
testcase: pathlib.Path,
|
160
|
+
validator: CodeItem,
|
161
|
+
validator_digest: str,
|
162
|
+
) -> TestcaseValidationInfo:
|
163
|
+
ok, message, _ = validate_test(testcase, validator, validator_digest)
|
164
|
+
info = TestcaseValidationInfo(
|
165
|
+
group='interactive',
|
166
|
+
path=testcase,
|
167
|
+
ok=ok,
|
168
|
+
hit_bounds={},
|
169
|
+
message=message,
|
170
|
+
)
|
171
|
+
return info
|
172
|
+
|
173
|
+
|
141
174
|
def compile_validators(
|
142
175
|
progress: Optional[StatusProgress] = None,
|
143
176
|
) -> Dict[str, str]:
|
@@ -202,6 +235,10 @@ def validate_testcases(
|
|
202
235
|
return validation_info
|
203
236
|
|
204
237
|
|
238
|
+
def has_validation_errors(infos: List[TestcaseValidationInfo]) -> bool:
|
239
|
+
return any(not info.ok for info in infos)
|
240
|
+
|
241
|
+
|
205
242
|
def print_validation_report(infos: List[TestcaseValidationInfo]):
|
206
243
|
console.console.rule('Validation report', style='status')
|
207
244
|
hit_bounds_per_group: Dict[str, HitBounds] = {}
|
@@ -218,6 +255,10 @@ def print_validation_report(infos: List[TestcaseValidationInfo]):
|
|
218
255
|
[hit_bounds_per_group[info.group], info.hit_bounds]
|
219
256
|
)
|
220
257
|
|
258
|
+
if not hit_bounds_per_group:
|
259
|
+
console.console.print()
|
260
|
+
return
|
261
|
+
|
221
262
|
if not _has_group_specific_validator():
|
222
263
|
hit_bounds_per_group = {None: _merge_hit_bounds(hit_bounds_per_group.values())}
|
223
264
|
|
rbx/clone.py
CHANGED
@@ -158,7 +158,7 @@ def main(lang: Optional[str] = None):
|
|
158
158
|
)
|
159
159
|
else:
|
160
160
|
console.print(
|
161
|
-
f'[status][rbx]rbx[/rbx] parsed problem from [item]{problem.url}[/item]
|
161
|
+
f'[status][rbx]rbx[/rbx] parsed problem from [item]{problem.url}[/item][/status]'
|
162
162
|
)
|
163
163
|
else:
|
164
164
|
batch_to_left[problem.batch.id] -= 1
|
rbx/grading/judge/digester.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import hashlib
|
2
|
+
import pathlib
|
2
3
|
from typing import IO
|
3
4
|
|
4
5
|
import gevent
|
@@ -33,3 +34,8 @@ def digest_cooperatively(f: IO[bytes], chunk_size: int = 2**20):
|
|
33
34
|
d = Digester()
|
34
35
|
digest_cooperatively_into_digester(f, d, chunk_size)
|
35
36
|
return d.digest()
|
37
|
+
|
38
|
+
|
39
|
+
def digest_file(path: pathlib.Path):
|
40
|
+
with open(path, 'rb') as f:
|
41
|
+
return digest_cooperatively(f)
|
rbx/grading/steps.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
import functools
|
2
|
+
import os
|
1
3
|
import pathlib
|
2
4
|
import shlex
|
5
|
+
import shutil
|
3
6
|
import subprocess
|
4
7
|
from enum import Enum
|
5
8
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
@@ -129,12 +132,6 @@ class TestcaseIO(BaseModel):
|
|
129
132
|
output: Optional[pathlib.Path] = None
|
130
133
|
|
131
134
|
|
132
|
-
class PreprocessLog(BaseModel):
|
133
|
-
cmd: List[str]
|
134
|
-
exitcode: int
|
135
|
-
log: str
|
136
|
-
|
137
|
-
|
138
135
|
class RunLogMetadata(BaseModel):
|
139
136
|
language: Optional[str] = None
|
140
137
|
|
@@ -151,6 +148,18 @@ class RunLog(BaseModel):
|
|
151
148
|
return None
|
152
149
|
return self.metadata.language
|
153
150
|
|
151
|
+
def get_summary(self) -> str:
|
152
|
+
if self.exitcode == 0:
|
153
|
+
return 'OK'
|
154
|
+
time = self.time or 0.0
|
155
|
+
memory = self.memory or 0
|
156
|
+
return f'FAILED with exit code {self.exitcode} and sandbox status {self.exitstatus} (time: {time}s, memory: {memory//(1024*1024)}MB)'
|
157
|
+
|
158
|
+
|
159
|
+
class PreprocessLog(RunLog):
|
160
|
+
cmd: List[str]
|
161
|
+
log: str
|
162
|
+
|
154
163
|
|
155
164
|
class TestcaseLog(RunLog):
|
156
165
|
stdout_absolute_path: Optional[pathlib.Path] = None
|
@@ -250,10 +259,26 @@ def _split_and_expand(command: str, sandbox: SandboxBase) -> List[str]:
|
|
250
259
|
return res
|
251
260
|
|
252
261
|
|
262
|
+
def _is_c_command(exe_command: str) -> bool:
|
263
|
+
return exe_command.endswith('gcc') or exe_command.endswith('clang')
|
264
|
+
|
265
|
+
|
253
266
|
def _is_cpp_command(exe_command: str) -> bool:
|
254
267
|
return exe_command.endswith('g++') or exe_command.endswith('clang++')
|
255
268
|
|
256
269
|
|
270
|
+
@functools.cache
|
271
|
+
def _complain_about_clang() -> None:
|
272
|
+
console.print(
|
273
|
+
'[warning]Notice your C++ files are being compiled with [item]clang[/item] instead of [item]g++[/item].[/warning]'
|
274
|
+
)
|
275
|
+
console.print('[warning]This may lead to unexpected behavior.[/warning]')
|
276
|
+
console.print('[warning]Consider using [item]g++[/item] instead.[/warning]')
|
277
|
+
console.print(
|
278
|
+
'[warning]See [item]https://rsalesc.github.io/rbx/cpp-on-macos[/item] for instructions on how to use [item]g++[/item] on MacOS.'
|
279
|
+
)
|
280
|
+
|
281
|
+
|
257
282
|
def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]:
|
258
283
|
cmds = shlex.split(command)
|
259
284
|
if not cmds:
|
@@ -274,6 +299,7 @@ def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]
|
|
274
299
|
if 'clang' not in lines[0]:
|
275
300
|
return None
|
276
301
|
|
302
|
+
_complain_about_clang()
|
277
303
|
bits = get_bits_stdcpp()
|
278
304
|
return GradingFileInput(src=bits, dest=pathlib.Path('bits/stdc++.h'))
|
279
305
|
|
@@ -288,12 +314,45 @@ def _maybe_get_bits_stdcpp_for_commands(
|
|
288
314
|
return None
|
289
315
|
|
290
316
|
|
317
|
+
@functools.cache
|
318
|
+
def _try_following_alias_for_exe(exe: str) -> Optional[str]:
|
319
|
+
if _is_c_command(exe) and os.environ.get('RBX_C_PATH'):
|
320
|
+
return os.environ['RBX_C_PATH']
|
321
|
+
if _is_cpp_command(exe) and os.environ.get('RBX_CXX_PATH'):
|
322
|
+
return os.environ['RBX_CXX_PATH']
|
323
|
+
output = subprocess.run(
|
324
|
+
f'which {exe}', shell=True, executable=shutil.which('bash'), capture_output=True
|
325
|
+
)
|
326
|
+
if output.returncode != 0:
|
327
|
+
return None
|
328
|
+
return output.stdout.decode().strip()
|
329
|
+
|
330
|
+
|
331
|
+
def _try_following_alias_for_command(command: str) -> str:
|
332
|
+
cmds = shlex.split(command)
|
333
|
+
if not cmds:
|
334
|
+
return command
|
335
|
+
exe = cmds[0]
|
336
|
+
new_exe = _try_following_alias_for_exe(exe)
|
337
|
+
if new_exe is None:
|
338
|
+
return command
|
339
|
+
return shlex.join([new_exe, *cmds[1:]])
|
340
|
+
|
341
|
+
|
342
|
+
def _try_following_alias_for_commands(commands: List[str]) -> List[str]:
|
343
|
+
res: List[str] = []
|
344
|
+
for command in commands:
|
345
|
+
res.append(_try_following_alias_for_command(command))
|
346
|
+
return res
|
347
|
+
|
348
|
+
|
291
349
|
def compile(
|
292
350
|
commands: List[str],
|
293
351
|
params: SandboxParams,
|
294
352
|
sandbox: SandboxBase,
|
295
353
|
artifacts: GradingArtifacts,
|
296
354
|
) -> bool:
|
355
|
+
commands = _try_following_alias_for_commands(commands)
|
297
356
|
bits_artifact = _maybe_get_bits_stdcpp_for_commands(commands)
|
298
357
|
if bits_artifact is not None:
|
299
358
|
_process_input_artifacts(GradingArtifacts(inputs=[bits_artifact]), sandbox)
|
@@ -337,6 +396,9 @@ def compile(
|
|
337
396
|
log = PreprocessLog(
|
338
397
|
cmd=cmd,
|
339
398
|
exitcode=sandbox.get_exit_code(),
|
399
|
+
exitstatus=sandbox.get_exit_status(),
|
400
|
+
time=sandbox.get_execution_time(),
|
401
|
+
memory=sandbox.get_memory_used(),
|
340
402
|
log='\n'.join(std_outputs),
|
341
403
|
)
|
342
404
|
logs.append(log)
|
@@ -346,10 +408,10 @@ def compile(
|
|
346
408
|
|
347
409
|
if logs and logs[-1].exitcode != 0:
|
348
410
|
console.print(
|
349
|
-
'
|
411
|
+
'[error]FAILED[/error] Preprocessing failed with command',
|
350
412
|
utils.highlight_json_obj(logs[-1].cmd),
|
351
413
|
)
|
352
|
-
console.print(f'
|
414
|
+
console.print(f'[error]Summary:[/error] {logs[-1].get_summary()}')
|
353
415
|
console.print(Text.from_ansi(logs[-1].log), style='default')
|
354
416
|
return False
|
355
417
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: rbx.cp
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.14
|
4
4
|
Summary:
|
5
5
|
Author: Roberto Sales
|
6
6
|
Requires-Python: >=3.9,<4.0
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
12
12
|
Requires-Dist: chardet (>=5.2.0,<6.0.0)
|
13
|
-
Requires-Dist: fastapi (>=0.
|
13
|
+
Requires-Dist: fastapi (>=0.115.8,<0.116.0)
|
14
14
|
Requires-Dist: filelock (>=3.14.0,<4.0.0)
|
15
15
|
Requires-Dist: gevent (>=24.2.1,<25.0.0)
|
16
16
|
Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
@@ -26,9 +26,9 @@ Requires-Dist: python-iso639 (>=2024.4.27,<2025.0.0)
|
|
26
26
|
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
27
27
|
Requires-Dist: questionary (>=2.1.0,<3.0.0)
|
28
28
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
29
|
-
Requires-Dist: rich (>=13.
|
29
|
+
Requires-Dist: rich (>=13.9.4,<14.0.0)
|
30
30
|
Requires-Dist: textual (>=0.79.1,<0.80.0)
|
31
|
-
Requires-Dist: typer
|
31
|
+
Requires-Dist: typer (>=0.15.1,<0.16.0)
|
32
32
|
Description-Content-Type: text/markdown
|
33
33
|
|
34
34
|
<p align="center">
|
@@ -2,27 +2,27 @@ rbx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
rbx/annotations.py,sha256=Z3jBUyZoXkrz34jko3Rft0bnMME6nWb0vsV5I3HlgR0,3064
|
3
3
|
rbx/autoenum.py,sha256=cusv8ClXRlDVvhZ8eDrtYcL_2peXlHugAey_ht8roXk,12025
|
4
4
|
rbx/box/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
rbx/box/builder.py,sha256=
|
6
|
-
rbx/box/cd.py,sha256=
|
7
|
-
rbx/box/checkers.py,sha256=
|
5
|
+
rbx/box/builder.py,sha256=QVKp7VL7YC8xs4oevcsam8DT_tjkSqu76nUoad1EwVY,3229
|
6
|
+
rbx/box/cd.py,sha256=NF0gqQso7a1IZzzZ6YJU8ip744AYAQ8Hp7vuzBRmZak,956
|
7
|
+
rbx/box/checkers.py,sha256=u5Pk-xViWxb_-KqAdEvnydbfppkYg0lBYoRQl5m_U_4,5380
|
8
8
|
rbx/box/code.py,sha256=0UfxzemrObXQq-cvAtSAImCm4sKV_4Xog-TMUZBRFwc,5761
|
9
|
-
rbx/box/compile.py,sha256=
|
9
|
+
rbx/box/compile.py,sha256=DHf9MNhfigXufe3Q5LghuTNsAYcpCoFho-6nG8F0t38,1315
|
10
10
|
rbx/box/conftest.py,sha256=sEmciXSeDC-wmrZ1JSxbsUenKNP_VWW32mrCun2pY3I,1070
|
11
11
|
rbx/box/contest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
12
|
rbx/box/contest/build_contest_statements.py,sha256=qar6h5u1xAlOKHl3P7-A1d0aYtaqu4PspGw_DYUGOLY,11260
|
13
|
-
rbx/box/contest/contest_package.py,sha256
|
13
|
+
rbx/box/contest/contest_package.py,sha256=GhZZSOC95wbypyeKFDgUIaaTjhldASudQBvjvurHbDw,2623
|
14
14
|
rbx/box/contest/contest_utils.py,sha256=vWv4iNWdJT5motAznfdNzl8o-tEoCU4xmdyaPTPJZuY,490
|
15
|
-
rbx/box/contest/main.py,sha256=
|
15
|
+
rbx/box/contest/main.py,sha256=FeTe_Qz9t233dTe1QQ6DxMDq5mcXHx59x9vgaLM8vTY,6976
|
16
16
|
rbx/box/contest/schema.py,sha256=rxzjJasMWPKKhvSJs4eW4A2oCiA4gXgfF-MzqsbPslQ,4914
|
17
|
-
rbx/box/contest/statements.py,sha256=
|
17
|
+
rbx/box/contest/statements.py,sha256=cRK5ndsywotUm66QqvtReqVAc-fW4c-2OZufK3Xd8_c,2947
|
18
18
|
rbx/box/creation.py,sha256=mVHVVj8ozX9D5qpkLewE5WSjF6HtTm74Pkwubk-bATg,2259
|
19
19
|
rbx/box/download.py,sha256=MFP-R26JiYGAP89I0TK-0fYc69Fsd20tsBqgtRCy5AE,2234
|
20
|
-
rbx/box/environment.py,sha256=
|
20
|
+
rbx/box/environment.py,sha256=GjwnJwtkTdkHmUh1b23zagjLsTiJQAOpP36A93mA-zc,11159
|
21
21
|
rbx/box/extensions.py,sha256=p0iLaU28KswOBDX2HGVO_dR2gk-JSAWb6sXC6GZ1d0w,738
|
22
|
-
rbx/box/generators.py,sha256=
|
22
|
+
rbx/box/generators.py,sha256=lKX_50Yf4bretHxgGzEZxKV7LFjpweaADC-22WEpLY4,16302
|
23
23
|
rbx/box/generators_test.py,sha256=mQqHepAMYa6zV_PseQALI0nIX6AdQktt6lh94muFhNw,1758
|
24
|
-
rbx/box/main.py,sha256=
|
25
|
-
rbx/box/package.py,sha256=
|
24
|
+
rbx/box/main.py,sha256=id1YpYKb7GHWoEMdiRAwfT08kY-ol52ulGcaCaPhtqw,16411
|
25
|
+
rbx/box/package.py,sha256=Hds0WIvhAWXnYOmIXGULcLLVeFM1YkllOPSKHB5QSkk,10532
|
26
26
|
rbx/box/packaging/boca/extension.py,sha256=hQhcbocNfW2ESv5RalS1wf6uvOoOfOnR_gHvbXUbSzY,852
|
27
27
|
rbx/box/packaging/boca/packager.py,sha256=FOhSRg5K5Y4qNB0WyTR3DKgrpObf9I0JbyGpJHOtxpo,10673
|
28
28
|
rbx/box/packaging/contest_main.py,sha256=ypiBS8dd0yCqoFJIqiK1Fo02dQmUB_G-Z7G926jomrk,2746
|
@@ -31,23 +31,23 @@ rbx/box/packaging/packager.py,sha256=suCT_SLnWa915rV2j8VFqzH43HGKRTr9mGGlrvj45aw
|
|
31
31
|
rbx/box/packaging/polygon/packager.py,sha256=CcjHzDr4MwSyp270gsPY6RWoz8bUJeykDqXPvQ3XZ1U,10773
|
32
32
|
rbx/box/packaging/polygon/test.py,sha256=bgEju5PwudgyfwxXJagm8fM6CJVlWM6l_-2q1V-oKaQ,3069
|
33
33
|
rbx/box/packaging/polygon/xml_schema.py,sha256=-r24bCeRMGLrGGoT9FIgmqr87xHL-JzrFaR6bztbYtw,2703
|
34
|
-
rbx/box/presets/__init__.py,sha256=
|
34
|
+
rbx/box/presets/__init__.py,sha256=Wiegp1onXPaZs8RE1J3PKT5j3PFWKw2U2rkgOSbnYeM,17529
|
35
35
|
rbx/box/presets/fetch.py,sha256=F-BCOlvEBEyDqtOhiDuGPn4EDtA4Bwm-fqHJ7zZGlW8,1975
|
36
36
|
rbx/box/presets/lock_schema.py,sha256=6sRPnyePOC8yy-5WcD5JRZdDJHf8loqbvpQ1IPiOU9s,349
|
37
37
|
rbx/box/presets/schema.py,sha256=mZmSPkQsw7eQM0lQN6er1MO_LiW1ObwwAZFDK0F5fxE,1962
|
38
38
|
rbx/box/schema.py,sha256=RBQmFxr35LfaRwbxR6WezeQQ3twqh9VS3tn20rlRm_I,12580
|
39
|
-
rbx/box/solutions.py,sha256=
|
39
|
+
rbx/box/solutions.py,sha256=zfRawpEufWOI2uw7jNFb86bx8HwU5UaiwQMUj9KlDBE,27447
|
40
40
|
rbx/box/solutions_test.py,sha256=DetQj7ZVnV3tBXBpCrFeK_Yv3XUtdEf29y_6qnyeyfY,1496
|
41
41
|
rbx/box/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
42
|
-
rbx/box/statements/build_statements.py,sha256=
|
42
|
+
rbx/box/statements/build_statements.py,sha256=24ZUM5H33NhmqnDL0ofA1anUKWxuZG1paA1tCV4AKns,11911
|
43
43
|
rbx/box/statements/builders.py,sha256=W3VkmtjfzrT5MkIVXgfR9fM-OWK007ihm5hfzvp9cfc,10474
|
44
44
|
rbx/box/statements/joiners.py,sha256=ZbxomnMjEFT8yf5WSWUB4tBa3DL3AhjGEuh8uqHyDdg,2837
|
45
45
|
rbx/box/statements/latex.py,sha256=LkcHwXjMFxbw--Gj9T1VkFKQFsXhY9dN7xZHpZycNW8,1346
|
46
46
|
rbx/box/statements/latex_jinja.py,sha256=7WBfn1h8DpqCAmSE6Av64HfURMnJ2AO4QX1CD72sz5E,7096
|
47
47
|
rbx/box/statements/schema.py,sha256=g3KgBn4nIqx-0utH8R2FCqPmJP969chhYfn96chQgd4,3851
|
48
|
-
rbx/box/stresses.py,sha256=
|
48
|
+
rbx/box/stresses.py,sha256=DUc8TWFlVXUvh-iaBnvwVNmgoCi1puOJiKTHMP8COds,10595
|
49
49
|
rbx/box/stressing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
|
-
rbx/box/stressing/finder_parser.py,sha256=
|
50
|
+
rbx/box/stressing/finder_parser.py,sha256=syZGg_Wm762jAe2eN0oBCNeiM0H8lfZRWT5HKOXpkuQ,11917
|
51
51
|
rbx/box/stressing/generator_parser.py,sha256=oHZryjR3YohgaSO9WEirQ7b2e-98WgZStF0N99W4Thw,7380
|
52
52
|
rbx/box/testcases.py,sha256=bi7T5xXkwyWOkoI6ILcaf2gSSVuuNtZjhP5yL0DJAu4,1452
|
53
53
|
rbx/box/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -55,10 +55,10 @@ rbx/box/ui/captured_log.py,sha256=ptICDPViVnz-_2NfrcB0SSBXNW5L74zI-vAZNN7kSok,11
|
|
55
55
|
rbx/box/ui/css/app.tcss,sha256=apd5PkPEvl5jK3kE2qrxPyVED1VnvSsj08QQwzUPwEA,786
|
56
56
|
rbx/box/ui/main.py,sha256=b0rHcBF42W4AOCv7WhtiGf_rUnY0yxpqO5oj3wfR4R4,984
|
57
57
|
rbx/box/ui/run.py,sha256=wMEXrEFdQvMHz2hRKAFIithTnTtaL0kNQZu0jKmb8jI,7060
|
58
|
-
rbx/box/validators.py,sha256=
|
58
|
+
rbx/box/validators.py,sha256=pRBYCJRbA_7FrthJG9tNHQyTxq7yBNuu8lTgd7irc3s,8309
|
59
59
|
rbx/box/validators_test.py,sha256=hriR6rD32Ouu64eKYYTPLZVvqMxXj7Q2h1l_JAefL7U,344
|
60
60
|
rbx/checker.py,sha256=pj1jO3my48ru-qugbER5onccANCjoR0-PaFe3H3VGEY,4118
|
61
|
-
rbx/clone.py,sha256=
|
61
|
+
rbx/clone.py,sha256=wpHyED0_7ST7LD3vj7HjXhzqEzlwh6dRQvKQVDYhGeU,6744
|
62
62
|
rbx/config.py,sha256=2B0PwgDaLjfs5PI8-kfDia6UVOAJq4rpWvb8VmweSXg,7360
|
63
63
|
rbx/conftest.py,sha256=ouilbOIpvX8jTEdCAiWT85CbdBQKUUf41BjmDI82u-Y,967
|
64
64
|
rbx/console.py,sha256=l0iulQH3_jQEm455W66TbDtC4a8owkWTHIIQpJaXofQ,715
|
@@ -69,7 +69,7 @@ rbx/grading/caching.py,sha256=oGPnKpk9NIUJKwDMsPbEF0bMUtjHN8CEeAOvCzfM5dk,11848
|
|
69
69
|
rbx/grading/conftest.py,sha256=iN9LUG1IQqhK5JjkctcP68v6675oYsiD2sQSgyLMTqw,960
|
70
70
|
rbx/grading/judge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
71
71
|
rbx/grading/judge/cacher.py,sha256=W4bdPIvI8tXOl8A7B4Ncuzi9-zXCp0DX40i1XKQGOAU,17761
|
72
|
-
rbx/grading/judge/digester.py,sha256=
|
72
|
+
rbx/grading/judge/digester.py,sha256=m6o-kjwyFOXKdImUXtVbdMHhwrgrXk8FDnJFVefnTIw,951
|
73
73
|
rbx/grading/judge/sandbox.py,sha256=0h3YCmGabf9OfORJgx6v2Bed4kE-i8FyuZkPux-sDVk,23569
|
74
74
|
rbx/grading/judge/sandboxes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
75
75
|
rbx/grading/judge/sandboxes/isolate.py,sha256=9xgBuNfAvGtO2zME1FXRah2rcPvzDShsPG0TTuX_UDU,25649
|
@@ -78,7 +78,7 @@ rbx/grading/judge/sandboxes/timeit.py,sha256=rOQnxf90hhTrWOQ1Mt57HNhZAAn1jDULtMj
|
|
78
78
|
rbx/grading/judge/storage.py,sha256=FirqjwDqb0m0h2OTFyWrZL7CQ4XjZNxhqB4JpnDIhZY,9485
|
79
79
|
rbx/grading/judge/test.py,sha256=ll0Iw7zyOpGdKPD_PGH7dvUkb4stQLu-ikbQnqJvuAc,944
|
80
80
|
rbx/grading/judge/testiso.py,sha256=v14DtkWiZFJ9AKMzrb0_vZKPWDt8jz8iIw1Z2O-Advk,1397
|
81
|
-
rbx/grading/steps.py,sha256=
|
81
|
+
rbx/grading/steps.py,sha256=pJM3Xnog6ZXia2xvWMeJmNij_ipRg2hec_WBf1n5O44,18598
|
82
82
|
rbx/grading/steps_with_caching.py,sha256=C_IA_dStxp6poJyGggFFyEouU9y_739UOLKUiJITb8M,1489
|
83
83
|
rbx/grading/steps_with_caching_run_test.py,sha256=nRzB4OcXkb-kQ4WCj0iTGVfBACllxZ0Ek5RSwfoJRgo,15262
|
84
84
|
rbx/grading_utils.py,sha256=lL2KtSkOsMElqrRoApQTbFcqVOeHVWUDTMCa3IsLpC4,4484
|
@@ -157,8 +157,8 @@ rbx/testdata/caching/executable.py,sha256=WKRHNf_fprFJd1Fq1ubmQtR3mZzTYVNwKPLWuZ
|
|
157
157
|
rbx/testdata/compatible,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
158
158
|
rbx/testing_utils.py,sha256=ZZLKMUHlZ4HwsuNY50jqSBJ9HhpnFdba7opjDsvXE1U,2084
|
159
159
|
rbx/utils.py,sha256=wy8zQRb97n3lptilK7UxM4A44KjXb1W5Z1tD61a-K4A,4173
|
160
|
-
rbx_cp-0.5.
|
161
|
-
rbx_cp-0.5.
|
162
|
-
rbx_cp-0.5.
|
163
|
-
rbx_cp-0.5.
|
164
|
-
rbx_cp-0.5.
|
160
|
+
rbx_cp-0.5.14.dist-info/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
161
|
+
rbx_cp-0.5.14.dist-info/METADATA,sha256=1dncSdLwt6q_zdoeysRdfAlZVk4wfHXA4cDojM5d3kA,3249
|
162
|
+
rbx_cp-0.5.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
163
|
+
rbx_cp-0.5.14.dist-info/entry_points.txt,sha256=qBTLBOeifT1F00LWaEewRRE_jQPgvH7BUdJfZ-dYsFU,57
|
164
|
+
rbx_cp-0.5.14.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|