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/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 Callable, Dict, List, Optional, Set
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, group_path: pathlib.Path, subgroup_prefix: str, i: int
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
- _get_group_input(group_path, subgroup_prefix, i),
68
+ str(dest.inputPath),
56
69
  )
57
- if testcase.outputPath is not None and testcase.outputPath.is_file():
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
- _get_group_output(group_path, subgroup_prefix, i),
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 generate_output_for_testcase(
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
- def _get_necessary_generators(groups: Set[str], cacher: FileCacher) -> Set[str]:
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
- necessary_generators = set()
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 groups is not None and group.name not in groups:
298
+ if not visitor.should_visit_group(group.name):
272
299
  continue
273
300
 
274
- for generator_call in group.generators:
275
- necessary_generators.add(generator_call.name)
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
- if group.generatorScript is not None:
278
- script = _run_generator_script(group, cacher)
279
- for generator_name, _ in _extract_script_lines(script):
280
- necessary_generators.add(generator_name)
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
- call: GeneratorCall,
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
- ) -> GeneratorCall:
319
- # Generator args parser
320
- parsed_args = generator_parser.parse(call.args or '')
321
- vars = package.find_problem_package_or_die().expanded_vars
322
- generator_for_args = generator_parser.Generator(vars)
323
- expanded_args_str = generator_for_args.generate(parsed_args)
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
- generation_stderr = DigestHolder()
382
+ if spec.generator_call is not None:
383
+ call = spec.generator_call
326
384
 
327
- # Get generator item
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
- generation_log = run_item(
333
- generator,
334
- DigestOrSource.create(generator_digest),
335
- stdout=DigestOrDest.create(output),
336
- stderr=DigestOrDest.create(generation_stderr),
337
- extra_args=expanded_args_str or None,
338
- )
339
- if not generation_log or generation_log.exitcode != 0:
340
- console.console.print(
341
- f'[error]Failed generating test using generator call [info]{call.name} {expanded_args_str}[/info][/error]',
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
- raise typer.Exit(1)
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
- ok, message, *_ = validators.validate_test(output, validator, validator_digest)
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'[error]Failed validating testcase generated by call [info]{call.name} {expanded_args_str}[/info][/error]'
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
- def _generate_testcases_for_subgroup(
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
- cacher = package.get_file_cacher()
449
+ def step():
450
+ if progress is not None:
451
+ progress.step()
382
452
 
383
- group_path.mkdir(parents=True, exist_ok=True)
384
-
385
- i = 0
386
- # Individual testcases.
387
- for tc in subgroup.testcases or []:
388
- _copy_testcase_over(tc, group_path, subgroup_prefix, i)
389
- i += 1
390
- step()
391
-
392
- # Glob testcases.
393
- if subgroup.testcaseGlob:
394
- matched_inputs = sorted(PosixPath().glob(subgroup.testcaseGlob))
395
-
396
- for input_path in matched_inputs:
397
- if not input_path.is_file() or input_path.suffix != '.in':
398
- continue
399
- output_path = input_path.parent / f'{input_path.stem}.out'
400
- tc = Testcase(inputPath=input_path, outputPath=output_path)
401
- _copy_testcase_over(tc, group_path, subgroup_prefix, i)
402
- i += 1
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
- # Run single generators.
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
- _run_generator(
413
- generator,
414
- generator_call.args,
415
- compiled_generators[generator.name],
416
- group_path,
417
- subgroup_prefix,
418
- i,
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
- i += 1
421
- step()
422
-
423
- # Run generator script.
424
- if subgroup.generatorScript is not None:
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
- _run_generator(
435
- generator,
436
- args,
437
- compiled_generators[generator.name],
438
- group_path,
439
- subgroup_prefix,
440
- i,
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
- i += 1
443
- step()
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 generate_testcases(
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
- pkg = package.find_problem_package_or_die()
454
- cacher = package.get_file_cacher()
550
+ main_solution = package.get_main_solution()
551
+ solution_digest: Optional[str] = None
455
552
 
456
- compiled_generators = compile_generators(
457
- progress=progress,
458
- tracked_generators=_get_necessary_generators(groups, cacher)
459
- if groups is not None
460
- else None,
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
- testcases.clear_built_testcases()
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
- for testcase in pkg.testcases:
466
- if groups is not None and testcase.name not in groups:
467
- continue
468
- group_path = package.get_build_testgroup_path(testcase.name)
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
- if not testcase.subgroups:
471
- # Testcase group is itself a test subgroup.
472
- _generate_testcases_for_subgroup(
473
- testcase, group_path, '', compiled_generators, step
474
- )
475
- continue
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
- renamed_testcase = testcase.model_copy(update={'name': 'main'})
478
- subgroups = [renamed_testcase] + testcase.subgroups
479
- for i, subgroup in enumerate(subgroups):
480
- # Test subgroups were specified, use them.
481
- _generate_testcases_for_subgroup(
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