rbx.cp 0.5.28__py3-none-any.whl → 0.5.30__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 +5 -5
- rbx/box/code.py +50 -1
- rbx/box/contest/schema.py +5 -5
- rbx/box/creation.py +10 -0
- rbx/box/extensions.py +2 -2
- rbx/box/formatting.py +10 -0
- rbx/box/generators.py +405 -276
- rbx/box/generators_test.py +1 -1
- rbx/box/main.py +17 -7
- rbx/box/schema.py +51 -34
- rbx/box/setter_config.py +7 -7
- rbx/box/solutions.py +151 -62
- rbx/box/state.py +9 -0
- rbx/box/statements/schema.py +6 -4
- rbx/box/stresses.py +11 -4
- rbx/box/testcases.py +17 -1
- rbx/grading/judge/sandboxes/stupid_sandbox.py +0 -1
- rbx/utils.py +8 -2
- {rbx_cp-0.5.28.dist-info → rbx_cp-0.5.30.dist-info}/METADATA +1 -4
- {rbx_cp-0.5.28.dist-info → rbx_cp-0.5.30.dist-info}/RECORD +23 -21
- {rbx_cp-0.5.28.dist-info → rbx_cp-0.5.30.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.28.dist-info → rbx_cp-0.5.30.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.28.dist-info → rbx_cp-0.5.30.dist-info}/entry_points.txt +0 -0
rbx/box/generators.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
import abc
|
1
2
|
import pathlib
|
2
3
|
import shlex
|
3
4
|
import shutil
|
4
5
|
from pathlib import PosixPath
|
5
|
-
from typing import
|
6
|
+
from typing import Dict, List, Optional, Set
|
6
7
|
|
7
8
|
import typer
|
9
|
+
from pydantic import BaseModel
|
8
10
|
|
9
11
|
from rbx import console
|
10
12
|
from rbx.box import checkers, package, testcases, validators
|
@@ -15,14 +17,12 @@ from rbx.box.environment import (
|
|
15
17
|
)
|
16
18
|
from rbx.box.schema import (
|
17
19
|
CodeItem,
|
18
|
-
Generator,
|
19
20
|
GeneratorCall,
|
20
21
|
Testcase,
|
21
22
|
TestcaseSubgroup,
|
22
23
|
)
|
23
24
|
from rbx.box.stressing import generator_parser
|
24
|
-
from rbx.box.testcases import find_built_testcases
|
25
|
-
from rbx.grading.judge.cacher import FileCacher
|
25
|
+
from rbx.box.testcases import TestcaseEntry, find_built_testcases
|
26
26
|
from rbx.grading.steps import (
|
27
27
|
DigestHolder,
|
28
28
|
DigestOrDest,
|
@@ -47,51 +47,38 @@ def _get_group_output(
|
|
47
47
|
return group_path / f'{subgroup_prefix}{i:03d}.out'
|
48
48
|
|
49
49
|
|
50
|
+
def _fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
|
51
|
+
res = testcase.model_copy()
|
52
|
+
if res.outputPath is not None:
|
53
|
+
return res
|
54
|
+
output_path = res.inputPath.with_suffix('.ans')
|
55
|
+
if output_path.is_file():
|
56
|
+
res.outputPath = output_path
|
57
|
+
return res
|
58
|
+
|
59
|
+
|
50
60
|
def _copy_testcase_over(
|
51
|
-
testcase: Testcase,
|
61
|
+
testcase: Testcase,
|
62
|
+
dest: Testcase,
|
52
63
|
):
|
64
|
+
testcase = _fill_output_for_defined_testcase(testcase)
|
65
|
+
dest.inputPath.parent.mkdir(parents=True, exist_ok=True)
|
53
66
|
shutil.copy(
|
54
67
|
str(testcase.inputPath),
|
55
|
-
|
68
|
+
str(dest.inputPath),
|
56
69
|
)
|
57
|
-
if
|
70
|
+
if (
|
71
|
+
testcase.outputPath is not None
|
72
|
+
and testcase.outputPath.is_file()
|
73
|
+
and dest.outputPath is not None
|
74
|
+
):
|
75
|
+
dest.outputPath.parent.mkdir(parents=True, exist_ok=True)
|
58
76
|
shutil.copy(
|
59
77
|
str(testcase.outputPath),
|
60
|
-
|
78
|
+
str(dest.outputPath),
|
61
79
|
)
|
62
80
|
|
63
81
|
|
64
|
-
def _run_generator(
|
65
|
-
generator: Generator,
|
66
|
-
args: Optional[str],
|
67
|
-
compiled_digest: str,
|
68
|
-
group_path: pathlib.Path,
|
69
|
-
subgroup_prefix: str,
|
70
|
-
i: int = 0,
|
71
|
-
):
|
72
|
-
generation_stderr = DigestHolder()
|
73
|
-
run_log = run_item(
|
74
|
-
generator,
|
75
|
-
DigestOrSource.create(compiled_digest),
|
76
|
-
stdout=DigestOrDest.create(_get_group_input(group_path, subgroup_prefix, i)),
|
77
|
-
stderr=DigestOrDest.create(generation_stderr),
|
78
|
-
extra_args=args or None,
|
79
|
-
)
|
80
|
-
|
81
|
-
if not run_log or run_log.exitcode != 0:
|
82
|
-
console.console.print(
|
83
|
-
f'[error]Failed generating test {i} from group path {group_path}[/error]',
|
84
|
-
)
|
85
|
-
if run_log is not None:
|
86
|
-
console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
|
87
|
-
if generation_stderr.value is not None:
|
88
|
-
console.console.print('[error]Stderr:[/error]')
|
89
|
-
console.console.print(
|
90
|
-
package.get_digest_as_string(generation_stderr.value) or ''
|
91
|
-
)
|
92
|
-
raise typer.Exit(1)
|
93
|
-
|
94
|
-
|
95
82
|
def get_all_built_testcases() -> Dict[str, List[Testcase]]:
|
96
83
|
pkg = package.find_problem_package_or_die()
|
97
84
|
res = {group.name: find_built_testcases(group) for group in pkg.testcases}
|
@@ -103,108 +90,11 @@ def get_call_from_string(call_str: str) -> GeneratorCall:
|
|
103
90
|
return GeneratorCall(name=name, args=args)
|
104
91
|
|
105
92
|
|
106
|
-
def
|
107
|
-
main_solution_digest: str,
|
108
|
-
testcase: Testcase,
|
109
|
-
stderr_path: Optional[pathlib.Path] = None,
|
110
|
-
):
|
111
|
-
assert testcase.outputPath is not None
|
112
|
-
pkg = package.find_problem_package_or_die()
|
113
|
-
main_solution = package.get_main_solution()
|
114
|
-
if main_solution is None:
|
115
|
-
return
|
116
|
-
|
117
|
-
# Obey no limits when generating testcases.
|
118
|
-
sandbox = EnvironmentSandbox()
|
119
|
-
sandbox.fileSizeLimit = pkg.outputLimit
|
120
|
-
extra_config = ExecutionConfig(sandbox=sandbox)
|
121
|
-
|
122
|
-
try:
|
123
|
-
run_log = run_item(
|
124
|
-
main_solution,
|
125
|
-
DigestOrSource.create(main_solution_digest),
|
126
|
-
stdin=DigestOrSource.create(testcase.inputPath),
|
127
|
-
stdout=DigestOrDest.create(testcase.outputPath),
|
128
|
-
stderr=DigestOrDest.create(stderr_path)
|
129
|
-
if stderr_path is not None
|
130
|
-
else None,
|
131
|
-
extra_config=extra_config,
|
132
|
-
)
|
133
|
-
except:
|
134
|
-
console.console.print(
|
135
|
-
'[error]Failed running main solution to generate testcase.[/error]'
|
136
|
-
)
|
137
|
-
raise
|
138
|
-
|
139
|
-
if run_log is None or run_log.exitcode != 0:
|
140
|
-
console.console.print(
|
141
|
-
f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
|
142
|
-
)
|
143
|
-
if run_log is not None:
|
144
|
-
console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
|
145
|
-
checker_result = checkers.check_with_no_output(run_log)
|
146
|
-
console.console.print(
|
147
|
-
f'[warning]Verdict: [item]{checker_result.outcome.value}[/item][/warning]',
|
148
|
-
)
|
149
|
-
console.console.print(
|
150
|
-
f'[warning]Message: [info]{checker_result.message}[/info][/warning]',
|
151
|
-
)
|
152
|
-
console.console.print(f'Input written at [item]{testcase.inputPath}[/item]')
|
153
|
-
console.console.print(
|
154
|
-
f'Output written at [item]{testcase.outputPath}[/item]'
|
155
|
-
)
|
156
|
-
console.console.print(f'Stderr written at [item]{stderr_path}[/item]')
|
157
|
-
raise typer.Exit(1)
|
158
|
-
|
159
|
-
|
160
|
-
def generate_outputs_for_testcases(
|
161
|
-
progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
|
162
|
-
):
|
163
|
-
def step():
|
164
|
-
if progress is not None:
|
165
|
-
progress.step()
|
166
|
-
|
167
|
-
pkg = package.find_problem_package_or_die()
|
168
|
-
|
169
|
-
built_testcases = get_all_built_testcases()
|
170
|
-
main_solution = package.get_main_solution()
|
171
|
-
solution_digest: Optional[str] = None
|
172
|
-
|
173
|
-
if main_solution is not None:
|
174
|
-
if progress:
|
175
|
-
progress.update('Compiling main solution...')
|
176
|
-
try:
|
177
|
-
solution_digest = compile_item(main_solution)
|
178
|
-
except:
|
179
|
-
console.console.print('[error]Failed compiling main solution.[/error]')
|
180
|
-
raise
|
181
|
-
|
182
|
-
gen_runs_dir = package.get_problem_runs_dir() / '.gen'
|
183
|
-
shutil.rmtree(str(gen_runs_dir), ignore_errors=True)
|
184
|
-
gen_runs_dir.mkdir(parents=True, exist_ok=True)
|
185
|
-
|
186
|
-
for group in pkg.testcases:
|
187
|
-
if groups is not None and group.name not in groups:
|
188
|
-
continue
|
189
|
-
group_testcases = built_testcases[group.name]
|
190
|
-
|
191
|
-
for testcase in group_testcases:
|
192
|
-
stderr_path = gen_runs_dir / 'main.stderr'
|
193
|
-
|
194
|
-
assert testcase.outputPath is not None
|
195
|
-
if main_solution is None or solution_digest is None:
|
196
|
-
console.console.print(
|
197
|
-
'[error]No main solution found to generate outputs for testcases.[/error]',
|
198
|
-
)
|
199
|
-
raise typer.Exit(1)
|
200
|
-
|
201
|
-
generate_output_for_testcase(solution_digest, testcase, stderr_path)
|
202
|
-
step()
|
203
|
-
|
204
|
-
|
205
|
-
def _run_generator_script(testcase: TestcaseSubgroup, cacher: FileCacher) -> str:
|
93
|
+
def _run_generator_script(testcase: TestcaseSubgroup) -> str:
|
206
94
|
assert testcase.generatorScript is not None
|
207
95
|
|
96
|
+
cacher = package.get_file_cacher()
|
97
|
+
|
208
98
|
if not testcase.generatorScript.path.is_file():
|
209
99
|
console.console.print(
|
210
100
|
f'[error]Generator script not found: [item]{testcase.generatorScript.path}[/item][/error]'
|
@@ -262,22 +152,171 @@ def _extract_script_lines(script: str):
|
|
262
152
|
yield shlex.split(line)[0], shlex.join(shlex.split(line)[1:])
|
263
153
|
|
264
154
|
|
265
|
-
|
155
|
+
class GenerationMetadata(BaseModel):
|
156
|
+
copied_to: Testcase
|
157
|
+
|
158
|
+
copied_from: Optional[Testcase] = None
|
159
|
+
generator_call: Optional[GeneratorCall] = None
|
160
|
+
|
161
|
+
|
162
|
+
class GenerationTestcaseEntry(BaseModel):
|
163
|
+
group_entry: TestcaseEntry
|
164
|
+
subgroup_entry: TestcaseEntry
|
165
|
+
|
166
|
+
metadata: GenerationMetadata
|
167
|
+
|
168
|
+
|
169
|
+
class TestcaseVisitor(abc.ABC):
|
170
|
+
@abc.abstractmethod
|
171
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
172
|
+
pass
|
173
|
+
|
174
|
+
def should_visit_group(self, group_name: str) -> bool:
|
175
|
+
return True
|
176
|
+
|
177
|
+
def should_visit_subgroup(self, subgroup_path: str) -> bool:
|
178
|
+
return True
|
179
|
+
|
180
|
+
def should_visit_generator_scripts(
|
181
|
+
self, group_name: str, subgroup_path: str
|
182
|
+
) -> bool:
|
183
|
+
return True
|
184
|
+
|
185
|
+
|
186
|
+
class TestcaseGroupVisitor(TestcaseVisitor):
|
187
|
+
def __init__(self, groups: Optional[Set[str]] = None):
|
188
|
+
self.groups = groups
|
189
|
+
|
190
|
+
def should_visit_group(self, group_name: str) -> bool:
|
191
|
+
return self.groups is None or group_name in self.groups
|
192
|
+
|
193
|
+
|
194
|
+
def run_testcase_visitor(visitor: TestcaseVisitor):
|
266
195
|
pkg = package.find_problem_package_or_die()
|
267
|
-
existing_generators = set(generator.name for generator in pkg.generators)
|
268
196
|
|
269
|
-
|
197
|
+
def _explore_subgroup(
|
198
|
+
subgroup: TestcaseSubgroup, subgroup_index: Optional[int], prefix: List[str]
|
199
|
+
):
|
200
|
+
assert prefix and len(prefix) >= 1 and len(prefix) <= 2
|
201
|
+
group_path = prefix[0]
|
202
|
+
subgroup_path = '/'.join(prefix)
|
203
|
+
if not visitor.should_visit_subgroup(subgroup_path):
|
204
|
+
return
|
205
|
+
|
206
|
+
def _entry(i: int) -> TestcaseEntry:
|
207
|
+
return TestcaseEntry(group=group_path, index=i)
|
208
|
+
|
209
|
+
def _sub_entry(i: int) -> TestcaseEntry:
|
210
|
+
return TestcaseEntry(group=subgroup_path, index=i)
|
211
|
+
|
212
|
+
def _copied_to(i: int) -> Testcase:
|
213
|
+
group_fs_path = package.get_build_testgroup_path(group_path)
|
214
|
+
group_prefix = ''
|
215
|
+
if subgroup_index is not None:
|
216
|
+
group_prefix = f'{subgroup_index}-'
|
217
|
+
if len(prefix) == 2:
|
218
|
+
group_prefix += f'{prefix[1]}-'
|
219
|
+
return Testcase(
|
220
|
+
inputPath=_get_group_input(group_fs_path, group_prefix, i),
|
221
|
+
outputPath=_get_group_output(group_fs_path, group_prefix, i),
|
222
|
+
)
|
223
|
+
|
224
|
+
# Go through testcases.
|
225
|
+
i = 0
|
226
|
+
# Individual testcases.
|
227
|
+
for tc in subgroup.testcases or []:
|
228
|
+
visitor.visit(
|
229
|
+
GenerationTestcaseEntry(
|
230
|
+
group_entry=_entry(i),
|
231
|
+
subgroup_entry=_sub_entry(i),
|
232
|
+
metadata=GenerationMetadata(
|
233
|
+
copied_from=_fill_output_for_defined_testcase(tc),
|
234
|
+
copied_to=_copied_to(i),
|
235
|
+
),
|
236
|
+
)
|
237
|
+
)
|
238
|
+
i += 1
|
239
|
+
|
240
|
+
# Glob testcases.
|
241
|
+
if subgroup.testcaseGlob:
|
242
|
+
matched_inputs = sorted(PosixPath().glob(subgroup.testcaseGlob))
|
243
|
+
|
244
|
+
for input_path in matched_inputs:
|
245
|
+
if not input_path.is_file() or input_path.suffix != '.in':
|
246
|
+
continue
|
247
|
+
|
248
|
+
tc = Testcase(inputPath=input_path)
|
249
|
+
visitor.visit(
|
250
|
+
GenerationTestcaseEntry(
|
251
|
+
group_entry=_entry(i),
|
252
|
+
subgroup_entry=_sub_entry(i),
|
253
|
+
metadata=GenerationMetadata(
|
254
|
+
copied_from=_fill_output_for_defined_testcase(tc),
|
255
|
+
copied_to=_copied_to(i),
|
256
|
+
),
|
257
|
+
)
|
258
|
+
)
|
259
|
+
i += 1
|
260
|
+
|
261
|
+
# Single generators.
|
262
|
+
for generator_call in subgroup.generators:
|
263
|
+
visitor.visit(
|
264
|
+
GenerationTestcaseEntry(
|
265
|
+
group_entry=_entry(i),
|
266
|
+
subgroup_entry=_sub_entry(i),
|
267
|
+
metadata=GenerationMetadata(
|
268
|
+
generator_call=generator_call,
|
269
|
+
copied_to=_copied_to(i),
|
270
|
+
),
|
271
|
+
)
|
272
|
+
)
|
273
|
+
i += 1
|
274
|
+
|
275
|
+
if not visitor.should_visit_generator_scripts(group_path, subgroup_path):
|
276
|
+
return
|
277
|
+
|
278
|
+
# Run generator script.
|
279
|
+
if subgroup.generatorScript is not None:
|
280
|
+
script = _run_generator_script(subgroup)
|
281
|
+
|
282
|
+
# Run each line from generator script.
|
283
|
+
for generator_name, args in _extract_script_lines(script):
|
284
|
+
call = GeneratorCall(name=generator_name, args=args)
|
285
|
+
visitor.visit(
|
286
|
+
GenerationTestcaseEntry(
|
287
|
+
group_entry=_entry(i),
|
288
|
+
subgroup_entry=_sub_entry(i),
|
289
|
+
metadata=GenerationMetadata(
|
290
|
+
generator_call=call,
|
291
|
+
copied_to=_copied_to(i),
|
292
|
+
),
|
293
|
+
)
|
294
|
+
)
|
295
|
+
i += 1
|
296
|
+
|
270
297
|
for group in pkg.testcases:
|
271
|
-
if
|
298
|
+
if not visitor.should_visit_group(group.name):
|
272
299
|
continue
|
273
300
|
|
274
|
-
|
275
|
-
|
301
|
+
_explore_subgroup(group, 0 if group.subgroups else None, [group.name])
|
302
|
+
|
303
|
+
for i, subgroup in enumerate(group.subgroups):
|
304
|
+
_explore_subgroup(subgroup, i + 1, [group.name, subgroup.name])
|
276
305
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
306
|
+
|
307
|
+
def _get_necessary_generators_for_groups(
|
308
|
+
groups: Optional[Set[str]] = None,
|
309
|
+
) -> Set[str]:
|
310
|
+
pkg = package.find_problem_package_or_die()
|
311
|
+
existing_generators = set(generator.name for generator in pkg.generators)
|
312
|
+
necessary_generators = set()
|
313
|
+
|
314
|
+
class NecessaryGeneratorsVisitor(TestcaseGroupVisitor):
|
315
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
316
|
+
if entry.metadata.generator_call is not None:
|
317
|
+
necessary_generators.add(entry.metadata.generator_call.name)
|
318
|
+
|
319
|
+
run_testcase_visitor(NecessaryGeneratorsVisitor(groups))
|
281
320
|
|
282
321
|
return existing_generators.intersection(necessary_generators)
|
283
322
|
|
@@ -309,175 +348,265 @@ def compile_generators(
|
|
309
348
|
return generator_to_compiled_digest
|
310
349
|
|
311
350
|
|
351
|
+
def expand_generator_call(call: GeneratorCall) -> GeneratorCall:
|
352
|
+
vars = package.find_problem_package_or_die().expanded_vars
|
353
|
+
generator_for_args = generator_parser.Generator(vars)
|
354
|
+
parsed_args = generator_parser.parse(call.args or '')
|
355
|
+
return call.model_copy(update={'args': generator_for_args.generate(parsed_args)})
|
356
|
+
|
357
|
+
|
312
358
|
def generate_standalone(
|
313
|
-
|
314
|
-
output: pathlib.Path,
|
359
|
+
spec: GenerationMetadata,
|
315
360
|
validate: bool = True,
|
361
|
+
group_entry: Optional[TestcaseEntry] = None,
|
316
362
|
generator_digest: Optional[str] = None,
|
317
363
|
validator_digest: Optional[str] = None,
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
364
|
+
progress: Optional[StatusProgress] = None,
|
365
|
+
):
|
366
|
+
def _print_error_header(text: Optional[str] = None):
|
367
|
+
prefix = 'Failed generating test'
|
368
|
+
if group_entry is not None:
|
369
|
+
prefix += (
|
370
|
+
f' [item]{group_entry.group}[/item]/[item]{group_entry.index}[/item]'
|
371
|
+
)
|
372
|
+
suffix = '.'
|
373
|
+
if text:
|
374
|
+
suffix = f': {text}'
|
375
|
+
if spec.generator_call is not None:
|
376
|
+
console.console.print(
|
377
|
+
f'[error]{prefix} using generator call [info]{spec.generator_call.name} {spec.generator_call.args}[/info]{suffix}[/error]'
|
378
|
+
)
|
379
|
+
else:
|
380
|
+
console.console.print(f'[error]{prefix}{suffix}[/error]')
|
324
381
|
|
325
|
-
|
382
|
+
if spec.generator_call is not None:
|
383
|
+
call = spec.generator_call
|
326
384
|
|
327
|
-
|
328
|
-
generator = package.get_generator(call.name)
|
329
|
-
if generator_digest is None:
|
330
|
-
generator_digest = _compile_generator(generator)
|
385
|
+
generation_stderr = DigestHolder()
|
331
386
|
|
332
|
-
|
333
|
-
generator
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
)
|
343
|
-
if generation_log is not None:
|
344
|
-
console.console.print(
|
345
|
-
f'[error]Summary:[/error] {generation_log.get_summary()}'
|
346
|
-
)
|
347
|
-
if generation_stderr.value is not None:
|
348
|
-
console.console.print('[error]Stderr:[/error]')
|
349
|
-
console.console.print(
|
350
|
-
package.get_digest_as_string(generation_stderr.value) or ''
|
387
|
+
# Get generator item
|
388
|
+
generator = package.get_generator(call.name)
|
389
|
+
if generator_digest is None:
|
390
|
+
if progress:
|
391
|
+
progress.update(f'Compiling generator {generator.name}...')
|
392
|
+
generator_digest = _compile_generator(generator)
|
393
|
+
|
394
|
+
if progress:
|
395
|
+
progress.update(
|
396
|
+
f'Generating testcase [status]{generator.name} {call.args}[/status]...'
|
351
397
|
)
|
398
|
+
generation_log = run_item(
|
399
|
+
generator,
|
400
|
+
DigestOrSource.create(generator_digest),
|
401
|
+
stdout=DigestOrDest.create(spec.copied_to.inputPath),
|
402
|
+
stderr=DigestOrDest.create(generation_stderr),
|
403
|
+
extra_args=call.args or None,
|
404
|
+
)
|
405
|
+
if not generation_log or generation_log.exitcode != 0:
|
406
|
+
_print_error_header()
|
407
|
+
if generation_log is not None:
|
408
|
+
console.console.print(
|
409
|
+
f'[error]Summary:[/error] {generation_log.get_summary()}'
|
410
|
+
)
|
411
|
+
if generation_stderr.value is not None:
|
412
|
+
console.console.print('[error]Stderr:[/error]')
|
413
|
+
console.console.print(
|
414
|
+
package.get_digest_as_string(generation_stderr.value) or ''
|
415
|
+
)
|
352
416
|
|
353
|
-
|
417
|
+
raise typer.Exit(1)
|
418
|
+
elif spec.copied_from is not None:
|
419
|
+
_copy_testcase_over(spec.copied_from, spec.copied_to)
|
354
420
|
|
355
421
|
validator = package.get_validator_or_nil()
|
356
422
|
# Run validator, if it is available.
|
357
423
|
if validator is not None and validate:
|
358
424
|
if validator_digest is None:
|
425
|
+
if progress:
|
426
|
+
progress.update('Compiling validator...')
|
359
427
|
validator_tp = validators.compile_main_validator()
|
360
428
|
assert validator_tp is not None
|
361
429
|
_, validator_digest = validator_tp
|
362
|
-
|
430
|
+
if progress:
|
431
|
+
progress.update('Validating test...')
|
432
|
+
ok, message, *_ = validators.validate_test(
|
433
|
+
spec.copied_to.inputPath,
|
434
|
+
validator,
|
435
|
+
validator_digest,
|
436
|
+
)
|
363
437
|
if not ok:
|
438
|
+
_print_error_header('Failed validating testcase.')
|
439
|
+
console.console.print(f'[error]Message:[/error] {message}')
|
364
440
|
console.console.print(
|
365
|
-
f'
|
441
|
+
f'Testcase written at [item]{spec.copied_to.inputPath}[/item]'
|
366
442
|
)
|
367
|
-
console.console.print(f'[error]Message:[/error] {message}')
|
368
|
-
console.console.print(f'Testcase written at [item]{output}[/item]')
|
369
443
|
raise typer.Exit(1)
|
370
444
|
|
371
|
-
return call.model_copy(update={'args': expanded_args_str})
|
372
445
|
|
373
|
-
|
374
|
-
|
375
|
-
subgroup: TestcaseSubgroup,
|
376
|
-
group_path: pathlib.Path,
|
377
|
-
subgroup_prefix: str,
|
378
|
-
compiled_generators: Dict[str, str],
|
379
|
-
step: Callable,
|
446
|
+
def generate_testcases(
|
447
|
+
progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
|
380
448
|
):
|
381
|
-
|
449
|
+
def step():
|
450
|
+
if progress is not None:
|
451
|
+
progress.step()
|
382
452
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
453
|
+
compiled_generators = compile_generators(
|
454
|
+
progress=progress,
|
455
|
+
tracked_generators=_get_necessary_generators_for_groups(groups)
|
456
|
+
if groups is not None
|
457
|
+
else None,
|
458
|
+
)
|
459
|
+
|
460
|
+
testcases.clear_built_testcases()
|
461
|
+
|
462
|
+
class BuildTestcaseVisitor(TestcaseGroupVisitor):
|
463
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
464
|
+
if entry.metadata.copied_from is not None:
|
465
|
+
_copy_testcase_over(
|
466
|
+
entry.metadata.copied_from,
|
467
|
+
entry.metadata.copied_to,
|
468
|
+
)
|
469
|
+
|
470
|
+
if entry.metadata.generator_call is not None:
|
471
|
+
generate_standalone(
|
472
|
+
entry.metadata,
|
473
|
+
group_entry=entry.group_entry,
|
474
|
+
validate=False,
|
475
|
+
generator_digest=compiled_generators[
|
476
|
+
entry.metadata.generator_call.name
|
477
|
+
],
|
478
|
+
)
|
403
479
|
step()
|
404
480
|
|
405
|
-
|
406
|
-
for generator_call in subgroup.generators:
|
407
|
-
generator = package.get_generator(generator_call.name)
|
408
|
-
if generator.name not in compiled_generators:
|
409
|
-
console.console.print(f'Generator {generator.name} not compiled')
|
410
|
-
raise typer.Exit(1)
|
481
|
+
run_testcase_visitor(BuildTestcaseVisitor(groups))
|
411
482
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
483
|
+
|
484
|
+
def generate_output_for_testcase(
|
485
|
+
main_solution_digest: str,
|
486
|
+
testcase: Testcase,
|
487
|
+
stderr_path: Optional[pathlib.Path] = None,
|
488
|
+
):
|
489
|
+
assert testcase.outputPath is not None
|
490
|
+
|
491
|
+
if testcase.outputPath.is_file():
|
492
|
+
# Output file was already copied over from manual tests.
|
493
|
+
return
|
494
|
+
|
495
|
+
pkg = package.find_problem_package_or_die()
|
496
|
+
main_solution = package.get_main_solution()
|
497
|
+
if main_solution is None:
|
498
|
+
return
|
499
|
+
|
500
|
+
# Obey no limits when generating testcases.
|
501
|
+
sandbox = EnvironmentSandbox()
|
502
|
+
sandbox.fileSizeLimit = pkg.outputLimit
|
503
|
+
extra_config = ExecutionConfig(sandbox=sandbox)
|
504
|
+
|
505
|
+
try:
|
506
|
+
run_log = run_item(
|
507
|
+
main_solution,
|
508
|
+
DigestOrSource.create(main_solution_digest),
|
509
|
+
stdin=DigestOrSource.create(testcase.inputPath),
|
510
|
+
stdout=DigestOrDest.create(testcase.outputPath),
|
511
|
+
stderr=DigestOrDest.create(stderr_path)
|
512
|
+
if stderr_path is not None
|
513
|
+
else None,
|
514
|
+
extra_config=extra_config,
|
419
515
|
)
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
script = _run_generator_script(subgroup, cacher)
|
426
|
-
|
427
|
-
# Run each line from generator script.
|
428
|
-
for generator_name, args in _extract_script_lines(script):
|
429
|
-
generator = package.get_generator(generator_name)
|
430
|
-
if generator.name not in compiled_generators:
|
431
|
-
console.console.print(f'Generator {generator.name} not compiled')
|
432
|
-
raise typer.Exit(1)
|
516
|
+
except:
|
517
|
+
console.console.print(
|
518
|
+
'[error]Failed running main solution to generate testcase.[/error]'
|
519
|
+
)
|
520
|
+
raise
|
433
521
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
522
|
+
if run_log is None or run_log.exitcode != 0:
|
523
|
+
console.console.print(
|
524
|
+
f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
|
525
|
+
)
|
526
|
+
if run_log is not None:
|
527
|
+
console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
|
528
|
+
checker_result = checkers.check_with_no_output(run_log)
|
529
|
+
console.console.print(
|
530
|
+
f'[warning]Verdict: [item]{checker_result.outcome.value}[/item][/warning]',
|
441
531
|
)
|
442
|
-
|
443
|
-
|
532
|
+
console.console.print(
|
533
|
+
f'[warning]Message: [info]{checker_result.message}[/info][/warning]',
|
534
|
+
)
|
535
|
+
console.console.print(f'Input written at [item]{testcase.inputPath}[/item]')
|
536
|
+
console.console.print(
|
537
|
+
f'Output written at [item]{testcase.outputPath}[/item]'
|
538
|
+
)
|
539
|
+
console.console.print(f'Stderr written at [item]{stderr_path}[/item]')
|
540
|
+
raise typer.Exit(1)
|
444
541
|
|
445
542
|
|
446
|
-
def
|
543
|
+
def generate_outputs_for_testcases(
|
447
544
|
progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
|
448
545
|
):
|
449
546
|
def step():
|
450
547
|
if progress is not None:
|
451
548
|
progress.step()
|
452
549
|
|
453
|
-
|
454
|
-
|
550
|
+
main_solution = package.get_main_solution()
|
551
|
+
solution_digest: Optional[str] = None
|
455
552
|
|
456
|
-
|
457
|
-
progress
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
553
|
+
if main_solution is not None:
|
554
|
+
if progress:
|
555
|
+
progress.update('Compiling main solution...')
|
556
|
+
try:
|
557
|
+
solution_digest = compile_item(main_solution)
|
558
|
+
except:
|
559
|
+
console.console.print('[error]Failed compiling main solution.[/error]')
|
560
|
+
raise
|
462
561
|
|
463
|
-
|
562
|
+
gen_runs_dir = package.get_problem_runs_dir() / '.gen'
|
563
|
+
shutil.rmtree(str(gen_runs_dir), ignore_errors=True)
|
564
|
+
gen_runs_dir.mkdir(parents=True, exist_ok=True)
|
464
565
|
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
566
|
+
class GenerateOutputsVisitor(TestcaseGroupVisitor):
|
567
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
568
|
+
tc = entry.metadata.copied_to
|
569
|
+
if not tc.inputPath.is_file():
|
570
|
+
return
|
571
|
+
assert tc.outputPath is not None
|
469
572
|
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
573
|
+
if (
|
574
|
+
main_solution is None or solution_digest is None
|
575
|
+
) and not tc.outputPath.is_file():
|
576
|
+
console.console.print(
|
577
|
+
'[error]No main solution found to generate outputs for testcases.[/error]',
|
578
|
+
)
|
579
|
+
raise typer.Exit(1)
|
476
580
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
subgroup, group_path, f'{i}-{subgroup.name}-', compiled_generators, step
|
581
|
+
assert solution_digest is not None
|
582
|
+
generate_output_for_testcase(
|
583
|
+
solution_digest,
|
584
|
+
tc,
|
585
|
+
gen_runs_dir / 'main.stderr',
|
483
586
|
)
|
587
|
+
step()
|
588
|
+
|
589
|
+
run_testcase_visitor(GenerateOutputsVisitor(groups))
|
590
|
+
|
591
|
+
|
592
|
+
def extract_generation_testcases(
|
593
|
+
entries: List[TestcaseEntry],
|
594
|
+
) -> List[GenerationTestcaseEntry]:
|
595
|
+
# TODO: support subgroups.
|
596
|
+
groups = set(entry.group for entry in entries)
|
597
|
+
entry_keys = set(entry.key() for entry in entries)
|
598
|
+
|
599
|
+
res: List[GenerationTestcaseEntry] = []
|
600
|
+
|
601
|
+
class ExtractGenerationTestcasesVisitor(TestcaseVisitor):
|
602
|
+
def should_visit_group(self, group_name: str) -> bool:
|
603
|
+
return group_name in groups
|
604
|
+
|
605
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
606
|
+
# TODO: support subgroups.
|
607
|
+
if entry.group_entry.key() not in entry_keys:
|
608
|
+
return
|
609
|
+
res.append(entry)
|
610
|
+
|
611
|
+
run_testcase_visitor(ExtractGenerationTestcasesVisitor())
|
612
|
+
return res
|