rbx.cp 0.5.27__py3-none-any.whl → 0.5.29__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 +2 -0
- rbx/box/generators.py +403 -276
- rbx/box/main.py +10 -7
- rbx/box/retries.py +143 -0
- rbx/box/schema.py +15 -6
- rbx/box/setter_config.py +22 -0
- rbx/box/solutions.py +209 -101
- rbx/box/stresses.py +62 -33
- rbx/box/stressing/finder_parser.py +2 -2
- rbx/box/testcases.py +17 -1
- rbx/grading/steps.py +1 -0
- rbx/grading/steps_with_caching.py +5 -3
- rbx/resources/default_setter_config.mac.yml +8 -0
- rbx/resources/default_setter_config.yml +8 -0
- rbx/utils.py +8 -2
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/METADATA +1 -4
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/RECORD +21 -20
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.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_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('.out')
|
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_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,169 @@ 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: 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 len(prefix) == 2:
|
216
|
+
group_prefix = f'{subgroup_index}-{prefix[1]}-'
|
217
|
+
return Testcase(
|
218
|
+
inputPath=_get_group_input(group_fs_path, group_prefix, i),
|
219
|
+
outputPath=_get_group_output(group_fs_path, group_prefix, i),
|
220
|
+
)
|
221
|
+
|
222
|
+
# Go through testcases.
|
223
|
+
i = 0
|
224
|
+
# Individual testcases.
|
225
|
+
for tc in subgroup.testcases or []:
|
226
|
+
visitor.visit(
|
227
|
+
GenerationTestcaseEntry(
|
228
|
+
group_entry=_entry(i),
|
229
|
+
subgroup_entry=_sub_entry(i),
|
230
|
+
metadata=GenerationMetadata(
|
231
|
+
copied_from=_fill_output_for_testcase(tc),
|
232
|
+
copied_to=_copied_to(i),
|
233
|
+
),
|
234
|
+
)
|
235
|
+
)
|
236
|
+
i += 1
|
237
|
+
|
238
|
+
# Glob testcases.
|
239
|
+
if subgroup.testcaseGlob:
|
240
|
+
matched_inputs = sorted(PosixPath().glob(subgroup.testcaseGlob))
|
241
|
+
|
242
|
+
for input_path in matched_inputs:
|
243
|
+
if not input_path.is_file() or input_path.suffix != '.in':
|
244
|
+
continue
|
245
|
+
|
246
|
+
tc = Testcase(inputPath=input_path)
|
247
|
+
visitor.visit(
|
248
|
+
GenerationTestcaseEntry(
|
249
|
+
group_entry=_entry(i),
|
250
|
+
subgroup_entry=_sub_entry(i),
|
251
|
+
metadata=GenerationMetadata(
|
252
|
+
copied_from=_fill_output_for_testcase(tc),
|
253
|
+
copied_to=_copied_to(i),
|
254
|
+
),
|
255
|
+
)
|
256
|
+
)
|
257
|
+
i += 1
|
258
|
+
|
259
|
+
# Single generators.
|
260
|
+
for generator_call in subgroup.generators:
|
261
|
+
visitor.visit(
|
262
|
+
GenerationTestcaseEntry(
|
263
|
+
group_entry=_entry(i),
|
264
|
+
subgroup_entry=_sub_entry(i),
|
265
|
+
metadata=GenerationMetadata(
|
266
|
+
generator_call=generator_call,
|
267
|
+
copied_to=_copied_to(i),
|
268
|
+
),
|
269
|
+
)
|
270
|
+
)
|
271
|
+
i += 1
|
272
|
+
|
273
|
+
if not visitor.should_visit_generator_scripts(group_path, subgroup_path):
|
274
|
+
return
|
275
|
+
|
276
|
+
# Run generator script.
|
277
|
+
if subgroup.generatorScript is not None:
|
278
|
+
script = _run_generator_script(subgroup)
|
279
|
+
|
280
|
+
# Run each line from generator script.
|
281
|
+
for generator_name, args in _extract_script_lines(script):
|
282
|
+
call = GeneratorCall(name=generator_name, args=args)
|
283
|
+
visitor.visit(
|
284
|
+
GenerationTestcaseEntry(
|
285
|
+
group_entry=_entry(i),
|
286
|
+
subgroup_entry=_sub_entry(i),
|
287
|
+
metadata=GenerationMetadata(
|
288
|
+
generator_call=call,
|
289
|
+
copied_to=_copied_to(i),
|
290
|
+
),
|
291
|
+
)
|
292
|
+
)
|
293
|
+
i += 1
|
294
|
+
|
270
295
|
for group in pkg.testcases:
|
271
|
-
if
|
296
|
+
if not visitor.should_visit_group(group.name):
|
272
297
|
continue
|
273
298
|
|
274
|
-
|
275
|
-
|
299
|
+
_explore_subgroup(group, 0, [group.name])
|
300
|
+
|
301
|
+
for i, subgroup in enumerate(group.subgroups):
|
302
|
+
_explore_subgroup(subgroup, i, [group.name, subgroup.name])
|
276
303
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
304
|
+
|
305
|
+
def _get_necessary_generators_for_groups(
|
306
|
+
groups: Optional[Set[str]] = None,
|
307
|
+
) -> Set[str]:
|
308
|
+
pkg = package.find_problem_package_or_die()
|
309
|
+
existing_generators = set(generator.name for generator in pkg.generators)
|
310
|
+
necessary_generators = set()
|
311
|
+
|
312
|
+
class NecessaryGeneratorsVisitor(TestcaseGroupVisitor):
|
313
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
314
|
+
if entry.metadata.generator_call is not None:
|
315
|
+
necessary_generators.add(entry.metadata.generator_call.name)
|
316
|
+
|
317
|
+
run_testcase_visitor(NecessaryGeneratorsVisitor(groups))
|
281
318
|
|
282
319
|
return existing_generators.intersection(necessary_generators)
|
283
320
|
|
@@ -309,175 +346,265 @@ def compile_generators(
|
|
309
346
|
return generator_to_compiled_digest
|
310
347
|
|
311
348
|
|
349
|
+
def expand_generator_call(call: GeneratorCall) -> GeneratorCall:
|
350
|
+
vars = package.find_problem_package_or_die().expanded_vars
|
351
|
+
generator_for_args = generator_parser.Generator(vars)
|
352
|
+
parsed_args = generator_parser.parse(call.args or '')
|
353
|
+
return call.model_copy(update={'args': generator_for_args.generate(parsed_args)})
|
354
|
+
|
355
|
+
|
312
356
|
def generate_standalone(
|
313
|
-
|
314
|
-
output: pathlib.Path,
|
357
|
+
spec: GenerationMetadata,
|
315
358
|
validate: bool = True,
|
359
|
+
group_entry: Optional[TestcaseEntry] = None,
|
316
360
|
generator_digest: Optional[str] = None,
|
317
361
|
validator_digest: Optional[str] = None,
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
362
|
+
progress: Optional[StatusProgress] = None,
|
363
|
+
):
|
364
|
+
def _print_error_header(text: Optional[str] = None):
|
365
|
+
prefix = 'Failed generating test'
|
366
|
+
if group_entry is not None:
|
367
|
+
prefix += (
|
368
|
+
f' [item]{group_entry.group}[/item]/[item]{group_entry.index}[/item]'
|
369
|
+
)
|
370
|
+
suffix = '.'
|
371
|
+
if text:
|
372
|
+
suffix = f': {text}'
|
373
|
+
if spec.generator_call is not None:
|
374
|
+
console.console.print(
|
375
|
+
f'[error]{prefix} using generator call [info]{spec.generator_call.name} {spec.generator_call.args}[/info]{suffix}[/error]'
|
376
|
+
)
|
377
|
+
else:
|
378
|
+
console.console.print(f'[error]{prefix}{suffix}[/error]')
|
324
379
|
|
325
|
-
|
380
|
+
if spec.generator_call is not None:
|
381
|
+
call = spec.generator_call
|
326
382
|
|
327
|
-
|
328
|
-
generator = package.get_generator(call.name)
|
329
|
-
if generator_digest is None:
|
330
|
-
generator_digest = _compile_generator(generator)
|
383
|
+
generation_stderr = DigestHolder()
|
331
384
|
|
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 ''
|
385
|
+
# Get generator item
|
386
|
+
generator = package.get_generator(call.name)
|
387
|
+
if generator_digest is None:
|
388
|
+
if progress:
|
389
|
+
progress.update(f'Compiling generator {generator.name}...')
|
390
|
+
generator_digest = _compile_generator(generator)
|
391
|
+
|
392
|
+
if progress:
|
393
|
+
progress.update(
|
394
|
+
f'Generating testcase [status]{generator.name} {call.args}[/status]...'
|
351
395
|
)
|
396
|
+
generation_log = run_item(
|
397
|
+
generator,
|
398
|
+
DigestOrSource.create(generator_digest),
|
399
|
+
stdout=DigestOrDest.create(spec.copied_to.inputPath),
|
400
|
+
stderr=DigestOrDest.create(generation_stderr),
|
401
|
+
extra_args=call.args or None,
|
402
|
+
)
|
403
|
+
if not generation_log or generation_log.exitcode != 0:
|
404
|
+
_print_error_header()
|
405
|
+
if generation_log is not None:
|
406
|
+
console.console.print(
|
407
|
+
f'[error]Summary:[/error] {generation_log.get_summary()}'
|
408
|
+
)
|
409
|
+
if generation_stderr.value is not None:
|
410
|
+
console.console.print('[error]Stderr:[/error]')
|
411
|
+
console.console.print(
|
412
|
+
package.get_digest_as_string(generation_stderr.value) or ''
|
413
|
+
)
|
352
414
|
|
353
|
-
|
415
|
+
raise typer.Exit(1)
|
416
|
+
elif spec.copied_from is not None:
|
417
|
+
_copy_testcase_over(spec.copied_from, spec.copied_to)
|
354
418
|
|
355
419
|
validator = package.get_validator_or_nil()
|
356
420
|
# Run validator, if it is available.
|
357
421
|
if validator is not None and validate:
|
358
422
|
if validator_digest is None:
|
423
|
+
if progress:
|
424
|
+
progress.update('Compiling validator...')
|
359
425
|
validator_tp = validators.compile_main_validator()
|
360
426
|
assert validator_tp is not None
|
361
427
|
_, validator_digest = validator_tp
|
362
|
-
|
428
|
+
if progress:
|
429
|
+
progress.update('Validating test...')
|
430
|
+
ok, message, *_ = validators.validate_test(
|
431
|
+
spec.copied_to.inputPath,
|
432
|
+
validator,
|
433
|
+
validator_digest,
|
434
|
+
)
|
363
435
|
if not ok:
|
436
|
+
_print_error_header('Failed validating testcase.')
|
437
|
+
console.console.print(f'[error]Message:[/error] {message}')
|
364
438
|
console.console.print(
|
365
|
-
f'
|
439
|
+
f'Testcase written at [item]{spec.copied_to.inputPath}[/item]'
|
366
440
|
)
|
367
|
-
console.console.print(f'[error]Message:[/error] {message}')
|
368
|
-
console.console.print(f'Testcase written at [item]{output}[/item]')
|
369
441
|
raise typer.Exit(1)
|
370
442
|
|
371
|
-
return call.model_copy(update={'args': expanded_args_str})
|
372
443
|
|
373
|
-
|
374
|
-
|
375
|
-
subgroup: TestcaseSubgroup,
|
376
|
-
group_path: pathlib.Path,
|
377
|
-
subgroup_prefix: str,
|
378
|
-
compiled_generators: Dict[str, str],
|
379
|
-
step: Callable,
|
444
|
+
def generate_testcases(
|
445
|
+
progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
|
380
446
|
):
|
381
|
-
|
447
|
+
def step():
|
448
|
+
if progress is not None:
|
449
|
+
progress.step()
|
382
450
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
451
|
+
compiled_generators = compile_generators(
|
452
|
+
progress=progress,
|
453
|
+
tracked_generators=_get_necessary_generators_for_groups(groups)
|
454
|
+
if groups is not None
|
455
|
+
else None,
|
456
|
+
)
|
457
|
+
|
458
|
+
testcases.clear_built_testcases()
|
459
|
+
|
460
|
+
class BuildTestcaseVisitor(TestcaseGroupVisitor):
|
461
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
462
|
+
if entry.metadata.copied_from is not None:
|
463
|
+
_copy_testcase_over(
|
464
|
+
entry.metadata.copied_from,
|
465
|
+
entry.metadata.copied_to,
|
466
|
+
)
|
467
|
+
|
468
|
+
if entry.metadata.generator_call is not None:
|
469
|
+
generate_standalone(
|
470
|
+
entry.metadata,
|
471
|
+
group_entry=entry.group_entry,
|
472
|
+
validate=False,
|
473
|
+
generator_digest=compiled_generators[
|
474
|
+
entry.metadata.generator_call.name
|
475
|
+
],
|
476
|
+
)
|
403
477
|
step()
|
404
478
|
|
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)
|
479
|
+
run_testcase_visitor(BuildTestcaseVisitor(groups))
|
411
480
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
481
|
+
|
482
|
+
def generate_output_for_testcase(
|
483
|
+
main_solution_digest: str,
|
484
|
+
testcase: Testcase,
|
485
|
+
stderr_path: Optional[pathlib.Path] = None,
|
486
|
+
):
|
487
|
+
assert testcase.outputPath is not None
|
488
|
+
|
489
|
+
if testcase.outputPath.is_file():
|
490
|
+
# Output file was already copied over from manual tests.
|
491
|
+
return
|
492
|
+
|
493
|
+
pkg = package.find_problem_package_or_die()
|
494
|
+
main_solution = package.get_main_solution()
|
495
|
+
if main_solution is None:
|
496
|
+
return
|
497
|
+
|
498
|
+
# Obey no limits when generating testcases.
|
499
|
+
sandbox = EnvironmentSandbox()
|
500
|
+
sandbox.fileSizeLimit = pkg.outputLimit
|
501
|
+
extra_config = ExecutionConfig(sandbox=sandbox)
|
502
|
+
|
503
|
+
try:
|
504
|
+
run_log = run_item(
|
505
|
+
main_solution,
|
506
|
+
DigestOrSource.create(main_solution_digest),
|
507
|
+
stdin=DigestOrSource.create(testcase.inputPath),
|
508
|
+
stdout=DigestOrDest.create(testcase.outputPath),
|
509
|
+
stderr=DigestOrDest.create(stderr_path)
|
510
|
+
if stderr_path is not None
|
511
|
+
else None,
|
512
|
+
extra_config=extra_config,
|
419
513
|
)
|
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)
|
514
|
+
except:
|
515
|
+
console.console.print(
|
516
|
+
'[error]Failed running main solution to generate testcase.[/error]'
|
517
|
+
)
|
518
|
+
raise
|
433
519
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
520
|
+
if run_log is None or run_log.exitcode != 0:
|
521
|
+
console.console.print(
|
522
|
+
f'[error]Failed generating output for [item]{testcase.inputPath}[/item][/error]',
|
523
|
+
)
|
524
|
+
if run_log is not None:
|
525
|
+
console.console.print(f'[error]Summary:[/error] {run_log.get_summary()}')
|
526
|
+
checker_result = checkers.check_with_no_output(run_log)
|
527
|
+
console.console.print(
|
528
|
+
f'[warning]Verdict: [item]{checker_result.outcome.value}[/item][/warning]',
|
441
529
|
)
|
442
|
-
|
443
|
-
|
530
|
+
console.console.print(
|
531
|
+
f'[warning]Message: [info]{checker_result.message}[/info][/warning]',
|
532
|
+
)
|
533
|
+
console.console.print(f'Input written at [item]{testcase.inputPath}[/item]')
|
534
|
+
console.console.print(
|
535
|
+
f'Output written at [item]{testcase.outputPath}[/item]'
|
536
|
+
)
|
537
|
+
console.console.print(f'Stderr written at [item]{stderr_path}[/item]')
|
538
|
+
raise typer.Exit(1)
|
444
539
|
|
445
540
|
|
446
|
-
def
|
541
|
+
def generate_outputs_for_testcases(
|
447
542
|
progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
|
448
543
|
):
|
449
544
|
def step():
|
450
545
|
if progress is not None:
|
451
546
|
progress.step()
|
452
547
|
|
453
|
-
|
454
|
-
|
548
|
+
main_solution = package.get_main_solution()
|
549
|
+
solution_digest: Optional[str] = None
|
455
550
|
|
456
|
-
|
457
|
-
progress
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
551
|
+
if main_solution is not None:
|
552
|
+
if progress:
|
553
|
+
progress.update('Compiling main solution...')
|
554
|
+
try:
|
555
|
+
solution_digest = compile_item(main_solution)
|
556
|
+
except:
|
557
|
+
console.console.print('[error]Failed compiling main solution.[/error]')
|
558
|
+
raise
|
462
559
|
|
463
|
-
|
560
|
+
gen_runs_dir = package.get_problem_runs_dir() / '.gen'
|
561
|
+
shutil.rmtree(str(gen_runs_dir), ignore_errors=True)
|
562
|
+
gen_runs_dir.mkdir(parents=True, exist_ok=True)
|
464
563
|
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
564
|
+
class GenerateOutputsVisitor(TestcaseGroupVisitor):
|
565
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
566
|
+
tc = entry.metadata.copied_to
|
567
|
+
if not tc.inputPath.is_file():
|
568
|
+
return
|
569
|
+
assert tc.outputPath is not None
|
469
570
|
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
571
|
+
if (
|
572
|
+
main_solution is None or solution_digest is None
|
573
|
+
) and not tc.outputPath.is_file():
|
574
|
+
console.console.print(
|
575
|
+
'[error]No main solution found to generate outputs for testcases.[/error]',
|
576
|
+
)
|
577
|
+
raise typer.Exit(1)
|
476
578
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
subgroup, group_path, f'{i}-{subgroup.name}-', compiled_generators, step
|
579
|
+
assert solution_digest is not None
|
580
|
+
generate_output_for_testcase(
|
581
|
+
solution_digest,
|
582
|
+
tc,
|
583
|
+
gen_runs_dir / 'main.stderr',
|
483
584
|
)
|
585
|
+
step()
|
586
|
+
|
587
|
+
run_testcase_visitor(GenerateOutputsVisitor(groups))
|
588
|
+
|
589
|
+
|
590
|
+
def extract_generation_testcases(
|
591
|
+
entries: List[TestcaseEntry],
|
592
|
+
) -> List[GenerationTestcaseEntry]:
|
593
|
+
# TODO: support subgroups.
|
594
|
+
groups = set(entry.group for entry in entries)
|
595
|
+
entry_keys = set(entry.key() for entry in entries)
|
596
|
+
|
597
|
+
res: List[GenerationTestcaseEntry] = []
|
598
|
+
|
599
|
+
class ExtractGenerationTestcasesVisitor(TestcaseVisitor):
|
600
|
+
def should_visit_group(self, group_name: str) -> bool:
|
601
|
+
return group_name in groups
|
602
|
+
|
603
|
+
def visit(self, entry: GenerationTestcaseEntry):
|
604
|
+
# TODO: support subgroups.
|
605
|
+
if entry.group_entry.key() not in entry_keys:
|
606
|
+
return
|
607
|
+
res.append(entry)
|
608
|
+
|
609
|
+
run_testcase_visitor(ExtractGenerationTestcasesVisitor())
|
610
|
+
return res
|