rbx.cp 0.5.36__py3-none-any.whl → 0.5.38__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 +0 -1
- rbx/box/builder.py +5 -2
- rbx/box/contest/statements.py +3 -1
- rbx/box/generators.py +19 -319
- rbx/box/generators_test.py +1 -1
- rbx/box/lazy_importing_main.py +7 -0
- rbx/box/lazy_importing_test.py +25 -0
- rbx/box/main.py +12 -3
- rbx/box/packaging/contest_main.py +5 -1
- rbx/box/packaging/main.py +7 -3
- rbx/box/presets/__init__.py +6 -2
- rbx/box/schema.py +8 -1
- rbx/box/setter_config.py +0 -1
- rbx/box/solutions.py +3 -2
- rbx/box/solutions_test.py +1 -1
- rbx/box/statements/build_statements.py +3 -1
- rbx/box/statements/builders.py +7 -6
- rbx/box/statements/joiners.py +7 -6
- rbx/box/testcase_extractors.py +348 -0
- rbx/box/testcase_utils.py +10 -0
- rbx/box/testcases/main.py +4 -2
- rbx/box/validators.py +61 -33
- rbx/config.py +8 -2
- rbx/grading/judge/sandboxes/stupid_sandbox.py +0 -1
- rbx/testing_utils.py +0 -1
- rbx/utils.py +1 -4
- {rbx_cp-0.5.36.dist-info → rbx_cp-0.5.38.dist-info}/METADATA +1 -1
- {rbx_cp-0.5.36.dist-info → rbx_cp-0.5.38.dist-info}/RECORD +31 -28
- {rbx_cp-0.5.36.dist-info → rbx_cp-0.5.38.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.36.dist-info → rbx_cp-0.5.38.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.36.dist-info → rbx_cp-0.5.38.dist-info}/entry_points.txt +0 -0
rbx/box/solutions.py
CHANGED
@@ -7,7 +7,6 @@ import shutil
|
|
7
7
|
from collections.abc import Iterator
|
8
8
|
from typing import Dict, Iterable, List, Optional, Set, Tuple
|
9
9
|
|
10
|
-
import questionary
|
11
10
|
import rich
|
12
11
|
import rich.live
|
13
12
|
import rich.markup
|
@@ -29,7 +28,6 @@ from rbx.box.formatting import get_formatted_memory, get_formatted_time
|
|
29
28
|
from rbx.box.generators import (
|
30
29
|
GenerationMetadata,
|
31
30
|
expand_generator_call,
|
32
|
-
extract_generation_testcases,
|
33
31
|
generate_output_for_testcase,
|
34
32
|
generate_standalone,
|
35
33
|
)
|
@@ -42,6 +40,7 @@ from rbx.box.schema import (
|
|
42
40
|
Testcase,
|
43
41
|
TestcaseGroup,
|
44
42
|
)
|
43
|
+
from rbx.box.testcase_extractors import extract_generation_testcases
|
45
44
|
from rbx.box.testcase_utils import TestcaseEntry, find_built_testcases
|
46
45
|
from rbx.grading.steps import (
|
47
46
|
DigestOrDest,
|
@@ -674,6 +673,8 @@ def pick_solutions(tracked_solutions: Optional[Set[str]]) -> List[str]:
|
|
674
673
|
tracked_solutions = set(str(sol.path) for sol in pkg.solutions)
|
675
674
|
|
676
675
|
# Store in a separate list to maintain order with the package declaration.
|
676
|
+
import questionary
|
677
|
+
|
677
678
|
choices = [
|
678
679
|
questionary.Choice(title=_get_solution_repr(sol), value=str(sol.path))
|
679
680
|
for sol in pkg.solutions
|
rbx/box/solutions_test.py
CHANGED
@@ -5,7 +5,6 @@ import pytest
|
|
5
5
|
|
6
6
|
from rbx.box.environment import VerificationLevel
|
7
7
|
from rbx.box.generators import (
|
8
|
-
extract_generation_testcases_from_groups,
|
9
8
|
generate_outputs_for_testcases,
|
10
9
|
generate_testcases,
|
11
10
|
)
|
@@ -13,6 +12,7 @@ from rbx.box.solutions import (
|
|
13
12
|
convert_list_of_solution_evaluations_to_dict,
|
14
13
|
run_solutions,
|
15
14
|
)
|
15
|
+
from rbx.box.testcase_extractors import extract_generation_testcases_from_groups
|
16
16
|
from rbx.grading.steps import Outcome
|
17
17
|
|
18
18
|
|
@@ -6,7 +6,7 @@ from typing import Annotated, Dict, List, Optional, Tuple
|
|
6
6
|
import typer
|
7
7
|
|
8
8
|
from rbx import annotations, console
|
9
|
-
from rbx.box import
|
9
|
+
from rbx.box import environment, package
|
10
10
|
from rbx.box.schema import Package
|
11
11
|
from rbx.box.statements.builders import (
|
12
12
|
BUILDER_LIST,
|
@@ -333,6 +333,8 @@ def build(
|
|
333
333
|
):
|
334
334
|
# At most run the validators, only in samples.
|
335
335
|
if samples:
|
336
|
+
from rbx.box import builder
|
337
|
+
|
336
338
|
if not builder.build(
|
337
339
|
verification=verification,
|
338
340
|
groups=set(['samples']),
|
rbx/box/statements/builders.py
CHANGED
@@ -11,12 +11,6 @@ from pydantic import BaseModel
|
|
11
11
|
|
12
12
|
from rbx import console, utils
|
13
13
|
from rbx.box.schema import Package, Primitive, Testcase
|
14
|
-
from rbx.box.statements.latex import (
|
15
|
-
MAX_PDFLATEX_RUNS,
|
16
|
-
Latex,
|
17
|
-
decode_latex_output,
|
18
|
-
should_rerun,
|
19
|
-
)
|
20
14
|
from rbx.box.statements.latex_jinja import (
|
21
15
|
JinjaDictWrapper,
|
22
16
|
render_latex_template,
|
@@ -355,6 +349,13 @@ class TeX2PDFBuilder(StatementBuilder):
|
|
355
349
|
item: StatementBuilderItem,
|
356
350
|
verbose: bool = False,
|
357
351
|
) -> bytes:
|
352
|
+
from rbx.box.statements.latex import (
|
353
|
+
MAX_PDFLATEX_RUNS,
|
354
|
+
Latex,
|
355
|
+
decode_latex_output,
|
356
|
+
should_rerun,
|
357
|
+
)
|
358
|
+
|
358
359
|
latex = Latex(input.decode())
|
359
360
|
latex_result = latex.build_pdf(context.root)
|
360
361
|
pdf = latex_result.pdf
|
rbx/box/statements/joiners.py
CHANGED
@@ -10,12 +10,6 @@ from rbx.box.statements.builders import (
|
|
10
10
|
StatementBuilderContest,
|
11
11
|
StatementCodeLanguage,
|
12
12
|
)
|
13
|
-
from rbx.box.statements.latex import (
|
14
|
-
MAX_PDFLATEX_RUNS,
|
15
|
-
Latex,
|
16
|
-
decode_latex_output,
|
17
|
-
should_rerun,
|
18
|
-
)
|
19
13
|
from rbx.box.statements.schema import Joiner, JoinerType, JoinTexToPDF, StatementType
|
20
14
|
|
21
15
|
|
@@ -84,6 +78,13 @@ class TeX2PDFJoiner(StatementJoiner):
|
|
84
78
|
contest: StatementBuilderContest,
|
85
79
|
verbose: bool = False,
|
86
80
|
) -> bytes:
|
81
|
+
from rbx.box.statements.latex import (
|
82
|
+
MAX_PDFLATEX_RUNS,
|
83
|
+
Latex,
|
84
|
+
decode_latex_output,
|
85
|
+
should_rerun,
|
86
|
+
)
|
87
|
+
|
87
88
|
latex = Latex(input.decode())
|
88
89
|
latex_result = latex.build_pdf(context.root)
|
89
90
|
pdf = latex_result.pdf
|
@@ -0,0 +1,348 @@
|
|
1
|
+
import abc
|
2
|
+
import pathlib
|
3
|
+
import shlex
|
4
|
+
from typing import Iterable, List, Optional, Set, Tuple
|
5
|
+
|
6
|
+
import typer
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from rbx import console
|
10
|
+
from rbx.box import package
|
11
|
+
from rbx.box.code import compile_item, run_item
|
12
|
+
from rbx.box.schema import CodeItem, GeneratorCall, Testcase, TestcaseSubgroup
|
13
|
+
from rbx.box.testcase_utils import (
|
14
|
+
TestcaseEntry,
|
15
|
+
TestcasePattern,
|
16
|
+
fill_output_for_defined_testcase,
|
17
|
+
)
|
18
|
+
from rbx.grading.steps import DigestHolder, DigestOrDest, DigestOrSource
|
19
|
+
|
20
|
+
|
21
|
+
def _get_group_input(
|
22
|
+
group_path: pathlib.Path, subgroup_prefix: str, i: int
|
23
|
+
) -> pathlib.Path:
|
24
|
+
return group_path / f'{subgroup_prefix}{i:03d}.in'
|
25
|
+
|
26
|
+
|
27
|
+
def _get_group_output(
|
28
|
+
group_path: pathlib.Path, subgroup_prefix: str, i: int
|
29
|
+
) -> pathlib.Path:
|
30
|
+
return group_path / f'{subgroup_prefix}{i:03d}.out'
|
31
|
+
|
32
|
+
|
33
|
+
def _run_generator_script(testcase: TestcaseSubgroup) -> str:
|
34
|
+
assert testcase.generatorScript is not None
|
35
|
+
|
36
|
+
cacher = package.get_file_cacher()
|
37
|
+
|
38
|
+
if not testcase.generatorScript.path.is_file():
|
39
|
+
console.console.print(
|
40
|
+
f'[error]Generator script not found: [item]{testcase.generatorScript.path}[/item][/error]'
|
41
|
+
)
|
42
|
+
raise typer.Exit(1)
|
43
|
+
|
44
|
+
script_digest = DigestHolder()
|
45
|
+
if testcase.generatorScript.path.suffix == '.txt':
|
46
|
+
script_digest.value = cacher.put_file_from_path(testcase.generatorScript.path)
|
47
|
+
else:
|
48
|
+
try:
|
49
|
+
compiled_digest = compile_item(testcase.generatorScript)
|
50
|
+
except:
|
51
|
+
console.console.print(
|
52
|
+
f'[error]Failed compiling generator script for group [item]{testcase.name}[/item].[/error]'
|
53
|
+
)
|
54
|
+
raise
|
55
|
+
|
56
|
+
run_stderr = DigestHolder()
|
57
|
+
run_log = run_item(
|
58
|
+
testcase.generatorScript,
|
59
|
+
DigestOrSource.create(compiled_digest),
|
60
|
+
stdout=DigestOrDest.create(script_digest),
|
61
|
+
stderr=DigestOrDest.create(run_stderr),
|
62
|
+
)
|
63
|
+
|
64
|
+
if run_log is None or run_log.exitcode != 0:
|
65
|
+
console.console.print(
|
66
|
+
f'Could not run generator script for group {testcase.name}'
|
67
|
+
)
|
68
|
+
if run_log is not None:
|
69
|
+
console.console.print(
|
70
|
+
f'[error]Summary:[/error] {run_log.get_summary()}'
|
71
|
+
)
|
72
|
+
if run_stderr.value is not None:
|
73
|
+
console.console.print('[error]Stderr:[/error]')
|
74
|
+
console.console.print(
|
75
|
+
package.get_digest_as_string(run_stderr.value) or ''
|
76
|
+
)
|
77
|
+
raise typer.Exit(1)
|
78
|
+
|
79
|
+
assert script_digest.value
|
80
|
+
script = cacher.get_file_content(script_digest.value).decode()
|
81
|
+
return script
|
82
|
+
|
83
|
+
|
84
|
+
def _extract_script_lines(script: str) -> Iterable[Tuple[str, str, int]]:
|
85
|
+
lines = script.splitlines()
|
86
|
+
for i, line in enumerate(lines):
|
87
|
+
line = line.strip()
|
88
|
+
if not line:
|
89
|
+
continue
|
90
|
+
if line.startswith('#'):
|
91
|
+
continue
|
92
|
+
yield shlex.split(line)[0], shlex.join(shlex.split(line)[1:]), i + 1
|
93
|
+
|
94
|
+
|
95
|
+
class GeneratorScriptEntry(BaseModel):
|
96
|
+
path: pathlib.Path
|
97
|
+
line: int
|
98
|
+
|
99
|
+
|
100
|
+
class GenerationMetadata(BaseModel):
|
101
|
+
copied_to: Testcase
|
102
|
+
|
103
|
+
copied_from: Optional[Testcase] = None
|
104
|
+
generator_call: Optional[GeneratorCall] = None
|
105
|
+
generator_script: Optional[GeneratorScriptEntry] = None
|
106
|
+
|
107
|
+
|
108
|
+
class GenerationTestcaseEntry(BaseModel):
|
109
|
+
group_entry: TestcaseEntry
|
110
|
+
subgroup_entry: TestcaseEntry
|
111
|
+
|
112
|
+
metadata: GenerationMetadata
|
113
|
+
validator: Optional[CodeItem] = None
|
114
|
+
extra_validators: List[CodeItem] = []
|
115
|
+
|
116
|
+
|
117
|
+
class TestcaseVisitor(abc.ABC):
|
118
|
+
@abc.abstractmethod
|
119
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
120
|
+
pass
|
121
|
+
|
122
|
+
def should_visit_group(self, group_name: str) -> bool:
|
123
|
+
return True
|
124
|
+
|
125
|
+
def should_visit_subgroup(self, subgroup_path: str) -> bool:
|
126
|
+
return True
|
127
|
+
|
128
|
+
def should_visit_generator_scripts(
|
129
|
+
self, group_name: str, subgroup_path: str
|
130
|
+
) -> bool:
|
131
|
+
return True
|
132
|
+
|
133
|
+
|
134
|
+
class TestcaseGroupVisitor(TestcaseVisitor):
|
135
|
+
def __init__(self, groups: Optional[Set[str]] = None):
|
136
|
+
self.groups = groups
|
137
|
+
|
138
|
+
def should_visit_group(self, group_name: str) -> bool:
|
139
|
+
return self.groups is None or group_name in self.groups
|
140
|
+
|
141
|
+
|
142
|
+
def run_testcase_visitor(visitor: TestcaseVisitor):
|
143
|
+
pkg = package.find_problem_package_or_die()
|
144
|
+
|
145
|
+
def _explore_subgroup(
|
146
|
+
subgroup: TestcaseSubgroup,
|
147
|
+
subgroup_index: Optional[int],
|
148
|
+
prefix: List[str],
|
149
|
+
validator: Optional[CodeItem] = None,
|
150
|
+
extra_validators: Optional[List[CodeItem]] = None,
|
151
|
+
):
|
152
|
+
extra_validators = extra_validators or []
|
153
|
+
|
154
|
+
assert prefix and len(prefix) >= 1 and len(prefix) <= 2
|
155
|
+
group_path = prefix[0]
|
156
|
+
subgroup_path = '/'.join(prefix)
|
157
|
+
if not visitor.should_visit_subgroup(subgroup_path):
|
158
|
+
return
|
159
|
+
|
160
|
+
def _entry(i: int) -> TestcaseEntry:
|
161
|
+
return TestcaseEntry(group=group_path, index=i)
|
162
|
+
|
163
|
+
def _sub_entry(i: int) -> TestcaseEntry:
|
164
|
+
return TestcaseEntry(group=subgroup_path, index=i)
|
165
|
+
|
166
|
+
def _copied_to(i: int) -> Testcase:
|
167
|
+
group_fs_path = package.get_build_testgroup_path(group_path)
|
168
|
+
group_prefix = ''
|
169
|
+
if subgroup_index is not None:
|
170
|
+
group_prefix = f'{subgroup_index}-'
|
171
|
+
if len(prefix) == 2:
|
172
|
+
group_prefix += f'{prefix[1]}-'
|
173
|
+
return Testcase(
|
174
|
+
inputPath=_get_group_input(group_fs_path, group_prefix, i),
|
175
|
+
outputPath=_get_group_output(group_fs_path, group_prefix, i),
|
176
|
+
)
|
177
|
+
|
178
|
+
# Go through testcases.
|
179
|
+
i = 0
|
180
|
+
# Individual testcases.
|
181
|
+
for tc in subgroup.testcases or []:
|
182
|
+
visitor.visit(
|
183
|
+
GenerationTestcaseEntry(
|
184
|
+
group_entry=_entry(i),
|
185
|
+
subgroup_entry=_sub_entry(i),
|
186
|
+
metadata=GenerationMetadata(
|
187
|
+
copied_from=fill_output_for_defined_testcase(tc),
|
188
|
+
copied_to=_copied_to(i),
|
189
|
+
),
|
190
|
+
validator=validator,
|
191
|
+
extra_validators=extra_validators,
|
192
|
+
)
|
193
|
+
)
|
194
|
+
i += 1
|
195
|
+
|
196
|
+
# Glob testcases.
|
197
|
+
if subgroup.testcaseGlob:
|
198
|
+
matched_inputs = sorted(pathlib.PosixPath().glob(subgroup.testcaseGlob))
|
199
|
+
|
200
|
+
for input_path in matched_inputs:
|
201
|
+
if not input_path.is_file() or input_path.suffix != '.in':
|
202
|
+
continue
|
203
|
+
|
204
|
+
tc = Testcase(inputPath=input_path)
|
205
|
+
visitor.visit(
|
206
|
+
GenerationTestcaseEntry(
|
207
|
+
group_entry=_entry(i),
|
208
|
+
subgroup_entry=_sub_entry(i),
|
209
|
+
metadata=GenerationMetadata(
|
210
|
+
copied_from=fill_output_for_defined_testcase(tc),
|
211
|
+
copied_to=_copied_to(i),
|
212
|
+
),
|
213
|
+
validator=validator,
|
214
|
+
extra_validators=extra_validators,
|
215
|
+
)
|
216
|
+
)
|
217
|
+
i += 1
|
218
|
+
|
219
|
+
# Single generators.
|
220
|
+
for generator_call in subgroup.generators:
|
221
|
+
visitor.visit(
|
222
|
+
GenerationTestcaseEntry(
|
223
|
+
group_entry=_entry(i),
|
224
|
+
subgroup_entry=_sub_entry(i),
|
225
|
+
metadata=GenerationMetadata(
|
226
|
+
generator_call=generator_call,
|
227
|
+
copied_to=_copied_to(i),
|
228
|
+
),
|
229
|
+
validator=validator,
|
230
|
+
extra_validators=extra_validators,
|
231
|
+
)
|
232
|
+
)
|
233
|
+
i += 1
|
234
|
+
|
235
|
+
if not visitor.should_visit_generator_scripts(group_path, subgroup_path):
|
236
|
+
return
|
237
|
+
|
238
|
+
# Run generator script.
|
239
|
+
if subgroup.generatorScript is not None:
|
240
|
+
script = _run_generator_script(subgroup)
|
241
|
+
|
242
|
+
# Run each line from generator script.
|
243
|
+
for generator_name, args, line_number in _extract_script_lines(script):
|
244
|
+
call = GeneratorCall(name=generator_name, args=args)
|
245
|
+
visitor.visit(
|
246
|
+
GenerationTestcaseEntry(
|
247
|
+
group_entry=_entry(i),
|
248
|
+
subgroup_entry=_sub_entry(i),
|
249
|
+
metadata=GenerationMetadata(
|
250
|
+
generator_call=call,
|
251
|
+
generator_script=GeneratorScriptEntry(
|
252
|
+
path=subgroup.generatorScript.path,
|
253
|
+
line=line_number,
|
254
|
+
),
|
255
|
+
copied_to=_copied_to(i),
|
256
|
+
),
|
257
|
+
validator=validator,
|
258
|
+
extra_validators=extra_validators,
|
259
|
+
)
|
260
|
+
)
|
261
|
+
i += 1
|
262
|
+
|
263
|
+
for group in pkg.testcases:
|
264
|
+
if not visitor.should_visit_group(group.name):
|
265
|
+
continue
|
266
|
+
|
267
|
+
group_validator = pkg.validator
|
268
|
+
if group.validator is not None:
|
269
|
+
group_validator = group.validator
|
270
|
+
|
271
|
+
extra_validators = group.extraValidators
|
272
|
+
_explore_subgroup(
|
273
|
+
group,
|
274
|
+
0 if group.subgroups else None,
|
275
|
+
[group.name],
|
276
|
+
validator=group_validator,
|
277
|
+
extra_validators=extra_validators,
|
278
|
+
)
|
279
|
+
|
280
|
+
for i, subgroup in enumerate(group.subgroups):
|
281
|
+
_explore_subgroup(
|
282
|
+
subgroup,
|
283
|
+
i + 1,
|
284
|
+
[group.name, subgroup.name],
|
285
|
+
validator=group_validator,
|
286
|
+
extra_validators=extra_validators + subgroup.extraValidators,
|
287
|
+
)
|
288
|
+
|
289
|
+
|
290
|
+
def extract_generation_testcases(
|
291
|
+
entries: List[TestcaseEntry],
|
292
|
+
) -> List[GenerationTestcaseEntry]:
|
293
|
+
# TODO: support subgroups.
|
294
|
+
groups = set(entry.group for entry in entries)
|
295
|
+
entry_keys = set(entry.key() for entry in entries)
|
296
|
+
|
297
|
+
res: List[GenerationTestcaseEntry] = []
|
298
|
+
|
299
|
+
class ExtractGenerationTestcasesVisitor(TestcaseVisitor):
|
300
|
+
def should_visit_group(self, group_name: str) -> bool:
|
301
|
+
return group_name in groups
|
302
|
+
|
303
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
304
|
+
# TODO: support subgroups.
|
305
|
+
if entry.group_entry.key() not in entry_keys:
|
306
|
+
return
|
307
|
+
res.append(entry)
|
308
|
+
|
309
|
+
run_testcase_visitor(ExtractGenerationTestcasesVisitor())
|
310
|
+
return res
|
311
|
+
|
312
|
+
|
313
|
+
def extract_generation_testcases_from_groups(
|
314
|
+
groups: Optional[Set[str]] = None,
|
315
|
+
) -> List[GenerationTestcaseEntry]:
|
316
|
+
res: List[GenerationTestcaseEntry] = []
|
317
|
+
|
318
|
+
class ExtractGenerationTestcasesVisitor(TestcaseGroupVisitor):
|
319
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
320
|
+
res.append(entry)
|
321
|
+
|
322
|
+
run_testcase_visitor(ExtractGenerationTestcasesVisitor(groups))
|
323
|
+
return res
|
324
|
+
|
325
|
+
|
326
|
+
def extract_generation_testcases_from_patterns(
|
327
|
+
patterns: List[TestcasePattern],
|
328
|
+
) -> List[GenerationTestcaseEntry]:
|
329
|
+
res: List[GenerationTestcaseEntry] = []
|
330
|
+
|
331
|
+
class ExtractGenerationTestcasesVisitor(TestcaseVisitor):
|
332
|
+
def should_visit_group(self, group_name: str) -> bool:
|
333
|
+
return any(pattern.intersecting_group(group_name) for pattern in patterns)
|
334
|
+
|
335
|
+
def should_visit_subgroup(self, subgroup_path: str) -> bool:
|
336
|
+
return any(
|
337
|
+
pattern.intersecting_group(subgroup_path) for pattern in patterns
|
338
|
+
)
|
339
|
+
|
340
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
341
|
+
if not any(
|
342
|
+
pattern.match(entry.group_entry) for pattern in patterns
|
343
|
+
) and not any(pattern.match(entry.subgroup_entry) for pattern in patterns):
|
344
|
+
return
|
345
|
+
res.append(entry)
|
346
|
+
|
347
|
+
run_testcase_visitor(ExtractGenerationTestcasesVisitor())
|
348
|
+
return res
|
rbx/box/testcase_utils.py
CHANGED
@@ -133,3 +133,13 @@ def get_samples() -> List[Testcase]:
|
|
133
133
|
)
|
134
134
|
for tc in tcs
|
135
135
|
]
|
136
|
+
|
137
|
+
|
138
|
+
def fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
|
139
|
+
res = testcase.model_copy()
|
140
|
+
if res.outputPath is not None:
|
141
|
+
return res
|
142
|
+
output_path = res.inputPath.with_suffix('.ans')
|
143
|
+
if output_path.is_file():
|
144
|
+
res.outputPath = output_path
|
145
|
+
return res
|
rbx/box/testcases/main.py
CHANGED
@@ -7,11 +7,13 @@ from rbx import annotations, config, utils
|
|
7
7
|
from rbx.box import package
|
8
8
|
from rbx.box.generators import (
|
9
9
|
GenerationTestcaseEntry,
|
10
|
-
extract_generation_testcases,
|
11
|
-
extract_generation_testcases_from_patterns,
|
12
10
|
generate_outputs_for_testcases,
|
13
11
|
generate_standalone,
|
14
12
|
)
|
13
|
+
from rbx.box.testcase_extractors import (
|
14
|
+
extract_generation_testcases,
|
15
|
+
extract_generation_testcases_from_patterns,
|
16
|
+
)
|
15
17
|
from rbx.box.testcase_utils import TestcaseEntry, TestcasePattern
|
16
18
|
from rbx.console import console
|
17
19
|
|
rbx/box/validators.py
CHANGED
@@ -9,7 +9,10 @@ from rbx import console
|
|
9
9
|
from rbx.box import package
|
10
10
|
from rbx.box.code import SanitizationLevel, compile_item, run_item
|
11
11
|
from rbx.box.schema import CodeItem, Primitive
|
12
|
-
from rbx.box.
|
12
|
+
from rbx.box.testcase_extractors import (
|
13
|
+
GenerationTestcaseEntry,
|
14
|
+
extract_generation_testcases_from_groups,
|
15
|
+
)
|
13
16
|
from rbx.grading.judge.sandbox import SandboxBase
|
14
17
|
from rbx.grading.steps import (
|
15
18
|
DigestHolder,
|
@@ -23,6 +26,7 @@ HitBounds = Dict[str, Tuple[bool, bool]]
|
|
23
26
|
|
24
27
|
|
25
28
|
class TestcaseValidationInfo(BaseModel):
|
29
|
+
validator: CodeItem
|
26
30
|
group: str
|
27
31
|
path: pathlib.Path
|
28
32
|
ok: bool
|
@@ -136,7 +140,7 @@ def _validate_testcase(
|
|
136
140
|
)
|
137
141
|
|
138
142
|
|
139
|
-
def
|
143
|
+
def _validate_test(
|
140
144
|
testcase: pathlib.Path,
|
141
145
|
validator: CodeItem,
|
142
146
|
validator_digest: str,
|
@@ -160,8 +164,9 @@ def validate_one_off(
|
|
160
164
|
validator: CodeItem,
|
161
165
|
validator_digest: str,
|
162
166
|
) -> TestcaseValidationInfo:
|
163
|
-
ok, message, _ =
|
167
|
+
ok, message, _ = _validate_test(testcase, validator, validator_digest)
|
164
168
|
info = TestcaseValidationInfo(
|
169
|
+
validator=validator,
|
165
170
|
group='interactive',
|
166
171
|
path=testcase,
|
167
172
|
ok=ok,
|
@@ -172,23 +177,29 @@ def validate_one_off(
|
|
172
177
|
|
173
178
|
|
174
179
|
def compile_validators(
|
180
|
+
validation_entries: List[GenerationTestcaseEntry],
|
175
181
|
progress: Optional[StatusProgress] = None,
|
176
182
|
) -> Dict[str, str]:
|
177
|
-
|
183
|
+
validators = []
|
184
|
+
|
185
|
+
for entry in validation_entries:
|
186
|
+
if entry.validator is not None:
|
187
|
+
validators.append(entry.validator)
|
188
|
+
validators.extend(entry.extra_validators)
|
178
189
|
|
179
|
-
|
190
|
+
validator_to_compiled_digest = {}
|
180
191
|
|
181
|
-
for
|
182
|
-
validator
|
183
|
-
if validator is None:
|
192
|
+
for validator in validators:
|
193
|
+
if str(validator.path) in validator_to_compiled_digest:
|
184
194
|
continue
|
195
|
+
|
185
196
|
if progress:
|
186
|
-
progress.update(
|
187
|
-
|
188
|
-
|
189
|
-
|
197
|
+
progress.update(f'Compiling validator [item]{validator.path}[/item]...')
|
198
|
+
validator_to_compiled_digest[str(validator.path)] = _compile_validator(
|
199
|
+
validator
|
200
|
+
)
|
190
201
|
|
191
|
-
return
|
202
|
+
return validator_to_compiled_digest
|
192
203
|
|
193
204
|
|
194
205
|
def validate_testcases(
|
@@ -199,38 +210,52 @@ def validate_testcases(
|
|
199
210
|
if progress is not None:
|
200
211
|
progress.step()
|
201
212
|
|
202
|
-
|
203
|
-
|
204
|
-
|
213
|
+
validation_entries = extract_generation_testcases_from_groups(groups)
|
214
|
+
validator_to_compiled_digest = compile_validators(
|
215
|
+
validation_entries, progress=progress
|
216
|
+
)
|
205
217
|
|
206
218
|
validation_info = []
|
207
219
|
|
208
|
-
for
|
209
|
-
|
210
|
-
if
|
211
|
-
continue
|
212
|
-
if group.name not in group_to_compiled_digest:
|
213
|
-
continue
|
214
|
-
if groups is not None and group.name not in groups:
|
220
|
+
for entry in validation_entries:
|
221
|
+
input_path = entry.metadata.copied_to.inputPath
|
222
|
+
if not input_path.is_file():
|
215
223
|
continue
|
216
|
-
compiled_digest = group_to_compiled_digest[group.name]
|
217
224
|
|
218
|
-
|
225
|
+
# Main validation.
|
226
|
+
if entry.validator is not None:
|
227
|
+
compiled_digest = validator_to_compiled_digest[str(entry.validator.path)]
|
228
|
+
ok, message, hit_bounds = _validate_test(
|
229
|
+
input_path, entry.validator, compiled_digest
|
230
|
+
)
|
231
|
+
validation_info.append(
|
232
|
+
TestcaseValidationInfo(
|
233
|
+
validator=entry.validator,
|
234
|
+
group=entry.group_entry.group,
|
235
|
+
path=input_path,
|
236
|
+
ok=ok,
|
237
|
+
hit_bounds=hit_bounds,
|
238
|
+
message=message,
|
239
|
+
)
|
240
|
+
)
|
219
241
|
|
220
|
-
for
|
221
|
-
|
222
|
-
|
242
|
+
for extra_validator in entry.extra_validators:
|
243
|
+
compiled_digest = validator_to_compiled_digest[str(extra_validator.path)]
|
244
|
+
ok, message, hit_bounds = _validate_test(
|
245
|
+
input_path, extra_validator, compiled_digest
|
223
246
|
)
|
224
247
|
validation_info.append(
|
225
248
|
TestcaseValidationInfo(
|
226
|
-
|
227
|
-
|
249
|
+
validator=extra_validator,
|
250
|
+
group=entry.group_entry.group,
|
251
|
+
path=input_path,
|
228
252
|
ok=ok,
|
229
253
|
hit_bounds=hit_bounds,
|
230
254
|
message=message,
|
231
255
|
)
|
232
256
|
)
|
233
|
-
|
257
|
+
|
258
|
+
step()
|
234
259
|
|
235
260
|
return validation_info
|
236
261
|
|
@@ -242,11 +267,14 @@ def has_validation_errors(infos: List[TestcaseValidationInfo]) -> bool:
|
|
242
267
|
def print_validation_report(infos: List[TestcaseValidationInfo]):
|
243
268
|
console.console.rule('Validation report', style='status')
|
244
269
|
hit_bounds_per_group: Dict[Optional[str], HitBounds] = {}
|
270
|
+
any_failure = False
|
245
271
|
for info in infos:
|
246
272
|
if not info.ok:
|
247
273
|
console.console.print(
|
248
|
-
f'[error]Testcase [item]{info.path}[/item] failed verification
|
274
|
+
f'[error]Testcase [item]{info.path}[/item] failed verification on validator [item]{info.validator.path}[/item]:[/error]'
|
249
275
|
)
|
276
|
+
console.console.print(info.message)
|
277
|
+
any_failure = True
|
250
278
|
continue
|
251
279
|
|
252
280
|
if info.group not in hit_bounds_per_group:
|
@@ -278,7 +306,7 @@ def print_validation_report(infos: List[TestcaseValidationInfo]):
|
|
278
306
|
# If there's only the samples group, do not check for hit bounds.
|
279
307
|
hit_bounds_per_group = {}
|
280
308
|
|
281
|
-
if not hit_bounds_per_group:
|
309
|
+
if not hit_bounds_per_group and not any_failure:
|
282
310
|
console.console.print('[info]No validation issues found.[/info]')
|
283
311
|
return
|
284
312
|
|