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/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_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, group_path: pathlib.Path, subgroup_prefix: str, i: int
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
- _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,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
- 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: 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 groups is not None and group.name not in groups:
296
+ if not visitor.should_visit_group(group.name):
272
297
  continue
273
298
 
274
- for generator_call in group.generators:
275
- necessary_generators.add(generator_call.name)
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
- 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)
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
- call: GeneratorCall,
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
- ) -> 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)
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
- generation_stderr = DigestHolder()
380
+ if spec.generator_call is not None:
381
+ call = spec.generator_call
326
382
 
327
- # Get generator item
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
- 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 ''
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
- raise typer.Exit(1)
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
- ok, message, *_ = validators.validate_test(output, validator, validator_digest)
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'[error]Failed validating testcase generated by call [info]{call.name} {expanded_args_str}[/info][/error]'
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
- 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,
444
+ def generate_testcases(
445
+ progress: Optional[StatusProgress] = None, groups: Optional[Set[str]] = None
380
446
  ):
381
- cacher = package.get_file_cacher()
447
+ def step():
448
+ if progress is not None:
449
+ progress.step()
382
450
 
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
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
- # 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)
479
+ run_testcase_visitor(BuildTestcaseVisitor(groups))
411
480
 
412
- _run_generator(
413
- generator,
414
- generator_call.args,
415
- compiled_generators[generator.name],
416
- group_path,
417
- subgroup_prefix,
418
- i,
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
- 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)
514
+ except:
515
+ console.console.print(
516
+ '[error]Failed running main solution to generate testcase.[/error]'
517
+ )
518
+ raise
433
519
 
434
- _run_generator(
435
- generator,
436
- args,
437
- compiled_generators[generator.name],
438
- group_path,
439
- subgroup_prefix,
440
- i,
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
- i += 1
443
- step()
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 generate_testcases(
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
- pkg = package.find_problem_package_or_die()
454
- cacher = package.get_file_cacher()
548
+ main_solution = package.get_main_solution()
549
+ solution_digest: Optional[str] = None
455
550
 
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
- )
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
- testcases.clear_built_testcases()
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
- 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)
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
- 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
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
- 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
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