rbx.cp 0.5.15__py3-none-any.whl → 0.5.17__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/solutions.py CHANGED
@@ -5,17 +5,17 @@ import dataclasses
5
5
  import pathlib
6
6
  import shutil
7
7
  from collections.abc import Iterator
8
- from typing import Dict, List, Optional, Set
8
+ from typing import Dict, Iterable, List, Optional, Set
9
9
 
10
10
  import rich
11
11
  import rich.live
12
12
  import rich.table
13
- from more_itertools import seekable
14
13
  from pydantic import BaseModel
15
14
 
16
15
  from rbx import console
17
16
  from rbx.box import checkers, environment, package
18
- from rbx.box.code import compile_item, run_item
17
+ from rbx.box.code import SanitizationLevel, compile_item, find_language_name, run_item
18
+ from rbx.box.deferred import Deferred
19
19
  from rbx.box.environment import EnvironmentSandbox, ExecutionConfig, VerificationLevel
20
20
  from rbx.box.generators import generate_output_for_testcase, generate_standalone
21
21
  from rbx.box.schema import (
@@ -36,14 +36,15 @@ from rbx.grading.steps import (
36
36
  )
37
37
  from rbx.utils import StatusProgress, model_to_yaml
38
38
 
39
- StructuredEvaluation = Dict[str, Dict[str, List[Optional[Evaluation]]]]
39
+ StructuredEvaluation = Dict[str, Dict[str, List[Optional[Deferred[Evaluation]]]]]
40
40
 
41
41
 
42
- class EvaluationItem(BaseModel):
42
+ @dataclasses.dataclass(frozen=True)
43
+ class EvaluationItem:
43
44
  solution_index: int
44
45
  group_name: str
45
46
  testcase_index: int
46
- eval: Evaluation
47
+ eval: Deferred[Evaluation]
47
48
 
48
49
 
49
50
  class GroupSkeleton(BaseModel):
@@ -54,7 +55,6 @@ class GroupSkeleton(BaseModel):
54
55
  class SolutionReportSkeleton(BaseModel):
55
56
  solutions: List[Solution]
56
57
  groups: List[GroupSkeleton]
57
- group_first: bool
58
58
 
59
59
  def find_group_skeleton(self, group_name: str) -> Optional[GroupSkeleton]:
60
60
  groups = [group for group in self.groups if group.name == group_name]
@@ -74,7 +74,7 @@ class SolutionReportSkeleton(BaseModel):
74
74
  @dataclasses.dataclass
75
75
  class RunSolutionResult:
76
76
  skeleton: SolutionReportSkeleton
77
- items: Iterator[EvaluationItem]
77
+ items: List[EvaluationItem]
78
78
 
79
79
  def empty_structured_evaluation(self) -> StructuredEvaluation:
80
80
  return self.skeleton.empty_structured_evaluation()
@@ -94,9 +94,18 @@ def get_matching_solutions(expected_outcome: ExpectedOutcome) -> List[Solution]:
94
94
  return res
95
95
 
96
96
 
97
+ def get_exact_matching_solutions(expected_outcome: ExpectedOutcome) -> List[Solution]:
98
+ res = []
99
+ for solution in package.get_solutions():
100
+ if solution.outcome == expected_outcome:
101
+ res.append(solution)
102
+ return res
103
+
104
+
97
105
  def compile_solutions(
98
106
  progress: Optional[StatusProgress] = None,
99
107
  tracked_solutions: Optional[Set[str]] = None,
108
+ sanitized: bool = False,
100
109
  ) -> Dict[pathlib.Path, str]:
101
110
  pkg = package.find_problem_package_or_die()
102
111
 
@@ -111,7 +120,12 @@ def compile_solutions(
111
120
  if progress:
112
121
  progress.update(f'Compiling solution [item]{solution.path}[/item]...')
113
122
  try:
114
- compiled_solutions[solution.path] = compile_item(solution)
123
+ compiled_solutions[solution.path] = compile_item(
124
+ solution,
125
+ sanitized=SanitizationLevel.FORCE
126
+ if sanitized
127
+ else SanitizationLevel.NONE,
128
+ )
115
129
  except:
116
130
  console.console.print(
117
131
  f'[error]Failed compiling solution [item]{solution.path}[/item][/error]'
@@ -129,11 +143,12 @@ def _run_solution_on_testcase(
129
143
  output_dir: pathlib.Path,
130
144
  testcase_index: int = 0,
131
145
  verification: VerificationLevel = VerificationLevel.NONE,
146
+ timelimit_override: Optional[int] = None,
132
147
  ) -> Evaluation:
133
148
  pkg = package.find_problem_package_or_die()
134
149
  actual_sandbox = package.get_singleton_sandbox()
135
150
 
136
- timelimit = pkg.timelimit_for_language(solution.language)
151
+ timelimit = timelimit_override or pkg.timelimit_for_language(solution.language)
137
152
 
138
153
  sandbox = EnvironmentSandbox()
139
154
  sandbox.timeLimit = timelimit
@@ -196,11 +211,13 @@ def _run_solution(
196
211
  group_name: str,
197
212
  progress: Optional[StatusProgress] = None,
198
213
  verification: VerificationLevel = VerificationLevel.NONE,
199
- ) -> Iterator[Evaluation]:
214
+ timelimit_override: Optional[int] = None,
215
+ ) -> List[Deferred[Evaluation]]:
200
216
  runs_dir = package.get_problem_runs_dir()
201
217
 
202
218
  group = package.get_testgroup(group_name)
203
219
  testcases = find_built_testcases(group)
220
+ res: List[Deferred[Evaluation]] = []
204
221
  for i, testcase in enumerate(testcases):
205
222
  assert testcase.outputPath is not None
206
223
  output_path = runs_dir / f'{solution_index}' / group.name
@@ -210,19 +227,25 @@ def _run_solution(
210
227
  f'Running solution [item]{solution.path}[/item] on test [item]{group.name}[/item] / [item]{i}[/item]...'
211
228
  )
212
229
 
213
- yield _run_solution_on_testcase(
214
- solution,
215
- compiled_digest,
216
- checker_digest,
217
- testcase,
218
- output_path,
219
- testcase_index=i,
220
- verification=verification,
221
- )
230
+ async def run_fn(i=i, testcase=testcase, output_path=output_path):
231
+ return _run_solution_on_testcase(
232
+ solution,
233
+ compiled_digest,
234
+ checker_digest,
235
+ testcase,
236
+ output_path,
237
+ testcase_index=i,
238
+ verification=verification,
239
+ timelimit_override=timelimit_override,
240
+ )
241
+
242
+ res.append(Deferred(run_fn))
243
+
244
+ return res
222
245
 
223
246
 
224
- def convert_list_of_solution_evaluations_to_dict(
225
- items: Iterator[EvaluationItem],
247
+ async def convert_list_of_solution_evaluations_to_dict(
248
+ items: Iterable[EvaluationItem],
226
249
  ) -> List[Dict[str, List[Evaluation]]]:
227
250
  pkg = package.find_problem_package_or_die()
228
251
  res: List[Dict[str, List[Evaluation]]] = [
@@ -230,14 +253,13 @@ def convert_list_of_solution_evaluations_to_dict(
230
253
  ]
231
254
 
232
255
  for item in items:
233
- res[item.solution_index][item.group_name].append(item.eval)
256
+ res[item.solution_index][item.group_name].append(await item.eval())
234
257
 
235
258
  return res
236
259
 
237
260
 
238
261
  def _get_report_skeleton(
239
262
  tracked_solutions: Optional[Set[str]] = None,
240
- group_first: bool = False,
241
263
  verification: VerificationLevel = VerificationLevel.NONE,
242
264
  ) -> SolutionReportSkeleton:
243
265
  pkg = package.find_problem_package_or_die()
@@ -258,7 +280,8 @@ def _get_report_skeleton(
258
280
  testcases = find_built_testcases(group)
259
281
  groups.append(GroupSkeleton(name=group.name, testcases=testcases))
260
282
  return SolutionReportSkeleton(
261
- solutions=solutions, groups=groups, group_first=group_first
283
+ solutions=solutions,
284
+ groups=groups,
262
285
  )
263
286
 
264
287
 
@@ -267,13 +290,14 @@ def _produce_solution_items(
267
290
  tracked_solutions: Optional[Set[str]] = None,
268
291
  verification: VerificationLevel = VerificationLevel.NONE,
269
292
  check: bool = True,
270
- group_first: bool = False,
271
- ) -> Iterator[EvaluationItem]:
293
+ timelimit_override: Optional[int] = None,
294
+ sanitized: bool = False,
295
+ ) -> List[EvaluationItem]:
272
296
  pkg = package.find_problem_package_or_die()
273
297
 
274
298
  checker_digest = checkers.compile_checker() if check else None
275
299
  compiled_solutions = compile_solutions(
276
- progress=progress, tracked_solutions=tracked_solutions
300
+ progress=progress, tracked_solutions=tracked_solutions, sanitized=sanitized
277
301
  )
278
302
 
279
303
  # Clear run directory and rely on cache to
@@ -293,7 +317,8 @@ def _produce_solution_items(
293
317
 
294
318
  def yield_items(
295
319
  solution_index: int, solution: Solution, group_name: str
296
- ) -> Iterator[EvaluationItem]:
320
+ ) -> List[EvaluationItem]:
321
+ res: List[EvaluationItem] = []
297
322
  for i, eval in enumerate(
298
323
  _run_solution(
299
324
  solution,
@@ -303,25 +328,28 @@ def _produce_solution_items(
303
328
  group_name,
304
329
  progress=progress,
305
330
  verification=verification,
331
+ timelimit_override=timelimit_override,
306
332
  )
307
333
  ):
308
- yield EvaluationItem(
309
- solution_index=solution_index,
310
- group_name=group_name,
311
- testcase_index=i,
312
- eval=eval,
334
+ res.append(
335
+ EvaluationItem(
336
+ solution_index=solution_index,
337
+ group_name=group_name,
338
+ testcase_index=i,
339
+ eval=eval,
340
+ )
313
341
  )
314
342
 
315
- groups = pkg.testcases
316
- if group_first:
317
- for group in groups:
318
- for i, solution in solutions:
319
- yield from yield_items(i, solution, group.name)
320
- return
343
+ return res
344
+
345
+ res: List[EvaluationItem] = []
321
346
 
347
+ groups = pkg.testcases
322
348
  for i, solution in solutions:
323
349
  for group in groups:
324
- yield from yield_items(i, solution, group.name)
350
+ res.extend(yield_items(i, solution, group.name))
351
+
352
+ return res
325
353
 
326
354
 
327
355
  def run_solutions(
@@ -329,18 +357,18 @@ def run_solutions(
329
357
  tracked_solutions: Optional[Set[str]] = None,
330
358
  verification: VerificationLevel = VerificationLevel.NONE,
331
359
  check: bool = True,
332
- group_first: bool = False,
360
+ timelimit_override: Optional[int] = None,
361
+ sanitized: bool = False,
333
362
  ) -> RunSolutionResult:
334
363
  return RunSolutionResult(
335
- skeleton=_get_report_skeleton(
336
- tracked_solutions, group_first, verification=verification
337
- ),
364
+ skeleton=_get_report_skeleton(tracked_solutions, verification=verification),
338
365
  items=_produce_solution_items(
339
366
  progress=progress,
340
367
  tracked_solutions=tracked_solutions,
341
368
  verification=verification,
342
369
  check=check,
343
- group_first=group_first,
370
+ timelimit_override=timelimit_override,
371
+ sanitized=sanitized,
344
372
  ),
345
373
  )
346
374
 
@@ -350,18 +378,26 @@ def _run_interactive_solutions(
350
378
  verification: VerificationLevel = VerificationLevel.NONE,
351
379
  generator: Optional[GeneratorCall] = None,
352
380
  check: bool = True,
381
+ sanitized: bool = False,
353
382
  ) -> Iterator[EvaluationItem]:
354
383
  pkg = package.find_problem_package_or_die()
355
384
  main_solution = package.get_main_solution()
356
385
  check = check and main_solution is not None
357
386
 
358
387
  checker_digest = checkers.compile_checker() if check else None
359
- compiled_solutions = compile_solutions(tracked_solutions=tracked_solutions)
388
+ compiled_solutions = compile_solutions(
389
+ tracked_solutions=tracked_solutions, sanitized=sanitized
390
+ )
360
391
 
361
392
  main_solution_digest = None
362
393
  if check and main_solution is not None:
363
394
  try:
364
- main_solution_digest = compile_item(main_solution)
395
+ main_solution_digest = compile_item(
396
+ main_solution,
397
+ sanitized=SanitizationLevel.FORCE
398
+ if sanitized
399
+ else SanitizationLevel.NONE,
400
+ )
365
401
  except:
366
402
  console.console.print(
367
403
  '[error]Failed compiling main solution. If you do not want to check against a main solution, run with --nocheck flag.[/error]'
@@ -399,27 +435,31 @@ def _run_interactive_solutions(
399
435
  for i, solution in solutions:
400
436
  output_dir = irun_dir / f'{i}'
401
437
 
402
- yield EvaluationItem(
403
- solution_index=i,
404
- group_name='irun',
405
- testcase_index=0,
406
- eval=_run_solution_on_testcase(
438
+ async def run_fn(solution=solution, output_dir=output_dir):
439
+ return _run_solution_on_testcase(
407
440
  solution,
408
441
  compiled_solutions[solution.path],
409
442
  checker_digest,
410
443
  testcase,
411
444
  output_dir,
412
445
  verification=verification,
413
- ),
446
+ )
447
+
448
+ yield EvaluationItem(
449
+ solution_index=i,
450
+ group_name='irun',
451
+ testcase_index=0,
452
+ eval=Deferred(run_fn),
414
453
  )
415
454
 
416
455
 
417
- def run_and_print_interactive_solutions(
456
+ async def run_and_print_interactive_solutions(
418
457
  tracked_solutions: Optional[Set[str]] = None,
419
458
  verification: VerificationLevel = VerificationLevel.NONE,
420
459
  generator: Optional[GeneratorCall] = None,
421
460
  check: bool = True,
422
461
  print: bool = False,
462
+ sanitized: bool = False,
423
463
  ):
424
464
  pkg = package.find_problem_package_or_die()
425
465
  items = _run_interactive_solutions(
@@ -427,16 +467,19 @@ def run_and_print_interactive_solutions(
427
467
  verification=verification,
428
468
  check=check,
429
469
  generator=generator,
470
+ sanitized=sanitized,
430
471
  )
431
472
 
432
473
  for item in items:
433
474
  sol = pkg.solutions[item.solution_index]
434
475
  _print_solution_header(sol, console.console)
435
476
 
436
- stdout_path = item.eval.log.stdout_absolute_path
477
+ eval = await item.eval()
478
+
479
+ stdout_path = eval.log.stdout_absolute_path
437
480
  if print:
438
481
  if (
439
- item.eval.testcase.output is not None
482
+ eval.testcase.output is not None
440
483
  and stdout_path is not None
441
484
  and stdout_path.is_file()
442
485
  ):
@@ -485,12 +528,6 @@ def _get_evals_time_in_ms(evals: List[Evaluation]) -> int:
485
528
  return max(int((eval.log.time or 0.0) * 1000) for eval in evals)
486
529
 
487
530
 
488
- def _get_evals_memory_in_mb(evals: List[Evaluation]) -> int:
489
- if not evals:
490
- return 0
491
- return max(int(eval.log.memory or 0) // (1024 * 1024) for eval in evals)
492
-
493
-
494
531
  def _get_evals_memory_in_bytes(evals: List[Evaluation]) -> int:
495
532
  if not evals:
496
533
  return 0
@@ -522,6 +559,7 @@ def _print_solution_outcome(
522
559
  has_plain_tle = False
523
560
  bad_verdicts = set()
524
561
  no_tle_bad_verdicts = set()
562
+ has_sanitizer_warnings = False
525
563
  for eval in evals:
526
564
  if eval.result.outcome != Outcome.ACCEPTED:
527
565
  bad_verdicts.add(eval.result.outcome)
@@ -534,7 +572,9 @@ def _print_solution_outcome(
534
572
  eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
535
573
  and eval.result.no_tle_outcome is None
536
574
  )
537
-
575
+ has_sanitizer_warnings = (
576
+ has_sanitizer_warnings or eval.result.sanitizer_warnings
577
+ )
538
578
  unmatched_bad_verdicts = set(
539
579
  v for v in bad_verdicts if not solution.outcome.match(v)
540
580
  )
@@ -556,7 +596,9 @@ def _print_solution_outcome(
556
596
 
557
597
  console.print()
558
598
  evals_time = _get_evals_time_in_ms(evals)
559
- expected_outcome_is_tle = solution.outcome.match(Outcome.TIME_LIMIT_EXCEEDED)
599
+ expected_outcome_is_tle = solution.outcome.match(
600
+ Outcome.TIME_LIMIT_EXCEEDED
601
+ ) and not solution.outcome.match(Outcome.ACCEPTED)
560
602
  if (
561
603
  # Running verification with double TL.
562
604
  verification.value >= VerificationLevel.FULL.value
@@ -583,15 +625,21 @@ def _print_solution_outcome(
583
625
  console.print(
584
626
  f'[yellow]WARNING[/yellow] The solution could still run under double TL, but failed with [item]{other_verdicts_names}[/item].'
585
627
  )
628
+
629
+ if has_sanitizer_warnings:
630
+ console.print(
631
+ '[warning]WARNING[/warning] The solution had sanitizer errors or warnings, marked with [item]*[/item]. See their stderr for more details.'
632
+ )
633
+
586
634
  console.print(f'Time: {get_evals_formatted_time(evals)}')
587
635
  console.print(f'Memory: {get_evals_formatted_memory(evals)}')
588
636
  return len(unmatched_bad_verdicts) == 0
589
637
 
590
638
 
591
639
  def _consume_and_key_evaluation_items(
592
- items: Iterator[EvaluationItem],
640
+ items: Iterable[EvaluationItem],
593
641
  skeleton: SolutionReportSkeleton,
594
- ) -> Iterator[StructuredEvaluation]:
642
+ ) -> StructuredEvaluation:
595
643
  """
596
644
  Consumes EvaluationItems from a run_solutions call and build a view
597
645
  with them, possibly marking with optional unprocessed items.
@@ -602,7 +650,8 @@ def _consume_and_key_evaluation_items(
602
650
  for item in items:
603
651
  solution = pkg.solutions[item.solution_index]
604
652
  res[str(solution.path)][item.group_name][item.testcase_index] = item.eval
605
- yield res
653
+
654
+ return res
606
655
 
607
656
 
608
657
  def _print_solution_header(solution: Solution, console: rich.console.Console):
@@ -615,18 +664,17 @@ def _print_solution_header(solution: Solution, console: rich.console.Console):
615
664
  console.print(f'({solution_testdir})')
616
665
 
617
666
 
618
- def _print_timing(
667
+ async def _print_timing(
619
668
  console: rich.console.Console,
620
669
  skeleton: SolutionReportSkeleton,
621
- structured_evaluation: StructuredEvaluation,
670
+ evaluations: StructuredEvaluation,
622
671
  ):
623
672
  slowest_good = None
624
673
  fastest_slow = None
625
674
  for solution in skeleton.solutions:
626
- evals_per_group = structured_evaluation[str(solution.path)]
627
675
  all_evals = []
628
- for evals in evals_per_group.values():
629
- all_evals.extend([eval for eval in evals if eval is not None])
676
+ for evals in evaluations[str(solution.path)].values():
677
+ all_evals.extend([await eval() for eval in evals if eval is not None])
630
678
  solution_time = _get_evals_time_in_ms(all_evals)
631
679
  if solution.outcome.match(Outcome.ACCEPTED):
632
680
  if slowest_good is None or solution_time > slowest_good:
@@ -645,16 +693,16 @@ def _print_timing(
645
693
  console.print(f'Fastest [error]slow[/error] solution: {fastest_slow} ms')
646
694
 
647
695
 
648
- def _render_detailed_group_table(
696
+ async def _render_detailed_group_table(
649
697
  group: TestcaseGroup,
650
698
  skeleton: SolutionReportSkeleton,
651
- structured_evaluations: Iterator[StructuredEvaluation],
699
+ structured_evaluations: StructuredEvaluation,
652
700
  console: rich.console.Console,
653
701
  ):
654
702
  group_skeleton = skeleton.find_group_skeleton(group.name)
655
703
  assert group_skeleton is not None
656
704
 
657
- def generate_table(
705
+ async def generate_table(
658
706
  structured_evaluation: StructuredEvaluation, group_name: str
659
707
  ) -> rich.table.Table:
660
708
  table = rich.table.Table()
@@ -666,12 +714,19 @@ def _render_detailed_group_table(
666
714
  row = []
667
715
  for solution in skeleton.solutions:
668
716
  eval = structured_evaluation[str(solution.path)][group_name][tc]
669
- evals_per_solution[str(solution.path)].append(eval)
670
717
  if eval is None:
671
718
  row.append('...')
672
719
  continue
720
+ eval = eval.peek()
721
+ if eval is None:
722
+ row.append('...')
723
+ continue
724
+
673
725
  verdict = get_testcase_markup_verdict(eval)
674
726
  time = get_evals_formatted_time([eval])
727
+ if eval.result.sanitizer_warnings:
728
+ time = f'{time} [item]*[/item]'
729
+ evals_per_solution[str(solution.path)].append(eval)
675
730
  row.append(f'{verdict} {time}')
676
731
  table.add_row(*row)
677
732
 
@@ -689,28 +744,30 @@ def _render_detailed_group_table(
689
744
  return table
690
745
 
691
746
  with rich.live.Live(
692
- generate_table(skeleton.empty_structured_evaluation(), group.name),
747
+ await generate_table(skeleton.empty_structured_evaluation(), group.name),
693
748
  refresh_per_second=5,
694
749
  console=console,
695
750
  ) as live:
696
- for _ in skeleton.solutions:
697
- for _ in group_skeleton.testcases:
698
- structured_evaluation = next(structured_evaluations)
699
- live.update(generate_table(structured_evaluation, group.name))
751
+ for solution in skeleton.solutions:
752
+ for tc, _ in enumerate(group_skeleton.testcases):
753
+ eval = structured_evaluations[str(solution.path)][group.name][tc]
754
+ if eval is None:
755
+ continue
756
+ await eval()
757
+ live.update(await generate_table(structured_evaluations, group.name))
700
758
  live.refresh()
701
759
 
702
760
 
703
- def _print_detailed_run_report(
761
+ async def _print_detailed_run_report(
704
762
  result: RunSolutionResult,
705
763
  console: rich.console.Console,
706
- structured_evaluations: Iterator[StructuredEvaluation],
764
+ structured_evaluations: StructuredEvaluation,
707
765
  timing: bool = True,
708
766
  ):
709
- structured_evaluations = seekable(structured_evaluations)
710
767
  for group in result.skeleton.groups:
711
768
  console.print(f'[bold][status]{group.name}[/status][/bold]')
712
769
 
713
- _render_detailed_group_table(
770
+ await _render_detailed_group_table(
714
771
  package.get_testgroup(group.name),
715
772
  result.skeleton,
716
773
  structured_evaluations,
@@ -719,12 +776,13 @@ def _print_detailed_run_report(
719
776
  continue
720
777
 
721
778
  ok = True
722
- structured_evaluations.seek(-1)
723
- structured_evaluation = next(structured_evaluations)
724
779
  for solution in result.skeleton.solutions:
725
780
  all_evals = []
726
- for evals in structured_evaluation[str(solution.path)].values():
781
+ for evals in structured_evaluations[str(solution.path)].values():
727
782
  all_evals.extend(evals)
783
+
784
+ # Resolve futures.
785
+ all_evals = [await eval() for eval in all_evals if eval is not None]
728
786
  _print_solution_header(solution, console)
729
787
  cur_ok = _print_solution_outcome(
730
788
  solution,
@@ -737,93 +795,123 @@ def _print_detailed_run_report(
737
795
  console.print()
738
796
 
739
797
  if timing:
740
- _print_timing(console, result.skeleton, structured_evaluation)
798
+ await _print_timing(console, result.skeleton, structured_evaluations)
741
799
  return ok
742
800
 
743
801
 
744
- def print_run_report(
802
+ async def print_run_report(
745
803
  result: RunSolutionResult,
746
804
  console: rich.console.Console,
747
805
  verification: environment.VerificationParam,
748
806
  detailed: bool = False,
749
807
  timing: bool = True,
750
808
  ) -> bool:
751
- pkg = package.find_problem_package_or_die()
752
- items = seekable(result.items)
753
- structured_evaluations = _consume_and_key_evaluation_items(items, result.skeleton)
809
+ structured_evaluations = _consume_and_key_evaluation_items(
810
+ result.items, result.skeleton
811
+ )
754
812
  if detailed:
755
- return _print_detailed_run_report(
813
+ return await _print_detailed_run_report(
756
814
  result, console, structured_evaluations, timing=timing
757
815
  )
758
816
 
759
- assert not result.skeleton.group_first
760
- # Since we're now streaming the evaluation results, the for-loop is a bit
761
- # confusing. We must keep state across the iteration to understand whether
762
- # we're seeing a new solution or a new testgroup.
763
817
  ok = True
764
- last_solution: Optional[Solution] = None
765
- last_group: Optional[str] = None
766
- test_index = 0
767
- all_evals = []
768
- group_evals = []
769
-
770
- def print_last_solution():
771
- nonlocal ok
772
- if last_solution is None:
773
- return
774
- cur_ok = _print_solution_outcome(
775
- last_solution,
776
- all_evals,
777
- console,
778
- verification=VerificationLevel(verification),
779
- )
780
- console.print()
781
- ok = ok and cur_ok
782
818
 
783
- for item in items:
784
- eval = item.eval
785
- solution = pkg.solutions[item.solution_index]
786
- is_new_solution = last_solution is None or solution.path != last_solution.path
787
- is_new_group = is_new_solution or last_group != item.group_name
788
- is_closing_group = last_group is not None and is_new_group
819
+ for solution in result.skeleton.solutions:
820
+ _print_solution_header(solution, console)
821
+ solution_evals = []
822
+ for group in result.skeleton.groups:
823
+ console.print(f'[bold][status]{group.name}[/status][/bold] ', end='')
824
+ group_evals = []
825
+ for i, _ in enumerate(group.testcases):
826
+ eval = structured_evaluations[str(solution.path)][group.name][i]
827
+ if eval is None:
828
+ continue
829
+ eval = await eval()
830
+ console.print(f'{i}/', end='')
831
+ console.print(get_testcase_markup_verdict(eval), end='')
832
+ if eval.result.sanitizer_warnings:
833
+ console.print('[item]*[/item]', end='')
834
+ console.print('', end=' ')
835
+ group_evals.append(eval)
836
+ solution_evals.append(eval)
789
837
 
790
- if is_closing_group:
791
838
  console.print(
792
839
  f'({get_evals_formatted_time(group_evals)}, {get_evals_formatted_memory(group_evals)})',
793
840
  end='',
794
841
  )
795
842
  console.print()
796
843
 
797
- if is_new_solution:
798
- print_last_solution()
799
- all_evals = []
800
- last_solution = solution
801
- _print_solution_header(last_solution, console)
844
+ ok = ok and _print_solution_outcome(
845
+ solution,
846
+ solution_evals,
847
+ console,
848
+ verification=VerificationLevel(verification),
849
+ )
850
+ console.print()
802
851
 
803
- if is_new_group:
804
- group_evals = []
805
- last_group = item.group_name
806
- test_index = 0
807
- console.print(f'[bold][status]{item.group_name}[/status][/bold]', end=' ')
852
+ await _print_timing(console, result.skeleton, structured_evaluations)
808
853
 
809
- all_evals.append(eval)
810
- group_evals.append(eval)
811
- console.print(f'{test_index}/', end='')
812
- console.print(get_testcase_markup_verdict(eval), end=' ')
854
+ return ok
813
855
 
814
- test_index += 1
815
856
 
816
- console.print(
817
- f'({get_evals_formatted_time(group_evals)}, {get_evals_formatted_memory(group_evals)})',
818
- end=' ',
857
+ async def estimate_time_limit(
858
+ console: rich.console.Console,
859
+ result: RunSolutionResult,
860
+ ) -> Optional[int]:
861
+ structured_evaluations = _consume_and_key_evaluation_items(
862
+ result.items, result.skeleton
819
863
  )
820
- console.print()
821
- print_last_solution()
822
864
 
823
- items.seek(0)
824
- structured_evaluations_list = list(structured_evaluations)
865
+ timing_per_solution = {}
866
+ timing_per_language = {}
825
867
 
826
- if structured_evaluations_list:
827
- _print_timing(console, result.skeleton, structured_evaluations_list[-1])
868
+ if not result.skeleton.solutions:
869
+ console.print('[error]No solutions to estimate time limit from.[/error]')
870
+ return None
828
871
 
829
- return ok
872
+ for solution in result.skeleton.solutions:
873
+ timings = []
874
+ for evals in structured_evaluations[str(solution.path)].values():
875
+ for eval in evals:
876
+ if eval is None:
877
+ continue
878
+ eval = await eval()
879
+ if eval.log.time is not None:
880
+ timings.append(int(eval.log.time * 1000))
881
+
882
+ if not timings:
883
+ console.print(
884
+ f'[warning]No timings for solution [item]{solution.path}[/item].[/warning]'
885
+ )
886
+ continue
887
+
888
+ timing_per_solution[str(solution.path)] = max(timings)
889
+ lang = find_language_name(solution)
890
+ if lang not in timing_per_language:
891
+ timing_per_language[lang] = 0
892
+ timing_per_language[lang] = max(timing_per_language[lang], max(timings))
893
+
894
+ console.rule('[status]Time estimation[/status]', style='status')
895
+
896
+ fastest_time = min(timing_per_solution.values())
897
+ slowest_time = max(timing_per_solution.values())
898
+
899
+ console.print(f'Fastest solution: {fastest_time} ms')
900
+ console.print(f'Slowest solution: {slowest_time} ms')
901
+
902
+ if len(timing_per_language) > 0:
903
+ timing_language_list = [(t, lang) for lang, t in timing_per_language.items()]
904
+ fastest_language_time, fastest_language = min(timing_language_list)
905
+ slowest_language_time, slowest_language = max(timing_language_list)
906
+
907
+ console.print(
908
+ f'Fastest language: {fastest_language} ({fastest_language_time} ms)'
909
+ )
910
+ console.print(
911
+ f'Slowest language: {slowest_language} ({slowest_language_time} ms)'
912
+ )
913
+
914
+ estimated_tl = int(max(fastest_time * 3, slowest_time * 1.5))
915
+ console.print(f'[success]Estimated time limit:[/success] {estimated_tl} ms')
916
+
917
+ return estimated_tl