rbx.cp 0.13.8__py3-none-any.whl → 0.16.0__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/__version__.py +1 -0
- rbx/box/cli.py +74 -70
- rbx/box/code.py +3 -0
- rbx/box/contest/build_contest_statements.py +65 -23
- rbx/box/contest/contest_package.py +8 -1
- rbx/box/contest/main.py +9 -3
- rbx/box/contest/schema.py +17 -13
- rbx/box/contest/statements.py +12 -8
- rbx/box/dump_schemas.py +2 -1
- rbx/box/environment.py +1 -1
- rbx/box/fields.py +22 -4
- rbx/box/generators.py +32 -13
- rbx/box/git_utils.py +29 -1
- rbx/box/limits_info.py +161 -0
- rbx/box/package.py +18 -1
- rbx/box/packaging/boca/boca_language_utils.py +26 -0
- rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
- rbx/box/packaging/boca/packager.py +7 -5
- rbx/box/packaging/contest_main.py +20 -12
- rbx/box/packaging/packager.py +24 -14
- rbx/box/packaging/polygon/packager.py +7 -3
- rbx/box/packaging/polygon/upload.py +2 -1
- rbx/box/presets/__init__.py +143 -78
- rbx/box/presets/fetch.py +10 -2
- rbx/box/presets/schema.py +16 -1
- rbx/box/remote.py +3 -3
- rbx/box/sanitizers/issue_stack.py +124 -0
- rbx/box/schema.py +87 -27
- rbx/box/solutions.py +74 -117
- rbx/box/statements/build_statements.py +12 -1
- rbx/box/statements/builders.py +5 -3
- rbx/box/statements/latex_jinja.py +73 -23
- rbx/box/statements/schema.py +7 -9
- rbx/box/stressing/generator_parser.py +3 -1
- rbx/box/tasks.py +10 -10
- rbx/box/testcase_extractors.py +8 -0
- rbx/box/testing/testing_preset.py +129 -2
- rbx/box/testing/testing_shared.py +3 -1
- rbx/box/timing.py +305 -0
- rbx/box/tooling/boca/debug_utils.py +88 -0
- rbx/box/tooling/boca/manual_scrape.py +20 -0
- rbx/box/tooling/boca/scraper.py +660 -57
- rbx/box/unit.py +0 -2
- rbx/box/validators.py +0 -4
- rbx/grading/judge/cacher.py +36 -0
- rbx/grading/judge/program.py +12 -2
- rbx/grading/judge/sandbox.py +1 -1
- rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
- rbx/grading/judge/storage.py +36 -3
- rbx/grading/limits.py +4 -0
- rbx/grading/steps.py +3 -2
- rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
- rbx/resources/presets/default/contest/statement/info.rbx.tex +46 -0
- rbx/resources/presets/default/preset.rbx.yml +1 -0
- rbx/resources/presets/default/problem/.gitignore +1 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
- rbx/resources/presets/default/problem/rbx.h +52 -5
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
- rbx/resources/presets/default/problem/testlib.h +6299 -0
- rbx/resources/presets/default/problem/validator.cpp +4 -3
- rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
- rbx/resources/presets/default/shared/icpc.sty +18 -3
- rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
- rbx/testing_utils.py +17 -1
- rbx/utils.py +45 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/METADATA +5 -2
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/RECORD +71 -67
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/entry_points.txt +0 -1
- rbx/providers/__init__.py +0 -43
- rbx/providers/codeforces.py +0 -73
- rbx/providers/provider.py +0 -26
- rbx/submitors/__init__.py +0 -18
- rbx/submitors/codeforces.py +0 -121
- rbx/submitors/submitor.py +0 -25
- /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/WHEEL +0 -0
rbx/box/remote.py
CHANGED
@@ -8,6 +8,7 @@ import typer
|
|
8
8
|
from rbx import console, utils
|
9
9
|
from rbx.box import cd, package
|
10
10
|
from rbx.box.formatting import href, ref
|
11
|
+
from rbx.box.tooling.boca.scraper import BocaRun
|
11
12
|
|
12
13
|
PathLike = Union[str, pathlib.Path]
|
13
14
|
|
@@ -69,11 +70,10 @@ class BocaExpander(Expander):
|
|
69
70
|
return None
|
70
71
|
run_number, site_number = match
|
71
72
|
|
73
|
+
run = BocaRun.from_run_number(run_number, site_number)
|
72
74
|
boca_uploader = boca_upload.get_boca_scraper()
|
73
75
|
boca_uploader.login()
|
74
|
-
sol_path = boca_uploader.download_run(
|
75
|
-
run_number, site_number, self.get_boca_folder()
|
76
|
-
)
|
76
|
+
sol_path = boca_uploader.download_run(run, self.get_boca_folder())
|
77
77
|
console.console.print(f'Downloaded {href(sol_path)} from BOCA...')
|
78
78
|
return sol_path
|
79
79
|
|
@@ -0,0 +1,124 @@
|
|
1
|
+
import contextvars
|
2
|
+
import enum
|
3
|
+
from collections import OrderedDict
|
4
|
+
from typing import Callable, Optional, Tuple, Union
|
5
|
+
|
6
|
+
from rbx import console
|
7
|
+
|
8
|
+
|
9
|
+
class IssueLevel(enum.Enum):
|
10
|
+
OVERVIEW = enum.auto()
|
11
|
+
DETAILED = enum.auto()
|
12
|
+
|
13
|
+
|
14
|
+
class Issue:
|
15
|
+
def get_detailed_section(self) -> Optional[Tuple[str, ...]]:
|
16
|
+
return None
|
17
|
+
|
18
|
+
def get_overview_section(self) -> Optional[Tuple[str, ...]]:
|
19
|
+
return None
|
20
|
+
|
21
|
+
def get_detailed_message(self) -> str:
|
22
|
+
return ''
|
23
|
+
|
24
|
+
def get_overview_message(self) -> str:
|
25
|
+
return ''
|
26
|
+
|
27
|
+
|
28
|
+
IssueSection = OrderedDict[str, Union[Issue, 'IssueSection']]
|
29
|
+
|
30
|
+
|
31
|
+
class IssueAccumulator:
|
32
|
+
def __init__(self):
|
33
|
+
self.issues = []
|
34
|
+
|
35
|
+
def add_issue(self, issue: Issue):
|
36
|
+
self.issues.append(issue)
|
37
|
+
|
38
|
+
def get_sections_by(
|
39
|
+
self, key: Callable[[Issue], Optional[Tuple[str, ...]]]
|
40
|
+
) -> IssueSection:
|
41
|
+
sections = OrderedDict()
|
42
|
+
for issue in self.issues:
|
43
|
+
section_key = key(issue)
|
44
|
+
if section_key is None:
|
45
|
+
continue
|
46
|
+
current = sections
|
47
|
+
for k in section_key[:-1]:
|
48
|
+
current = current.setdefault(k, OrderedDict())
|
49
|
+
current[section_key[-1]] = issue
|
50
|
+
return sections
|
51
|
+
|
52
|
+
def get_detailed_sections(self) -> IssueSection:
|
53
|
+
return self.get_sections_by(lambda issue: issue.get_detailed_section())
|
54
|
+
|
55
|
+
def get_overview_sections(self) -> IssueSection:
|
56
|
+
return self.get_sections_by(lambda issue: issue.get_overview_section())
|
57
|
+
|
58
|
+
def _print_report_by(
|
59
|
+
self,
|
60
|
+
section_fn: Callable[[], IssueSection],
|
61
|
+
message_fn: Callable[[Issue], str],
|
62
|
+
):
|
63
|
+
from rich.tree import Tree
|
64
|
+
|
65
|
+
tree = Tree('Issues')
|
66
|
+
sections = section_fn()
|
67
|
+
|
68
|
+
def print_section(section: IssueSection, tree: Tree):
|
69
|
+
for key, value in section.items():
|
70
|
+
child = tree.add(key)
|
71
|
+
if isinstance(value, OrderedDict):
|
72
|
+
print_section(value, child)
|
73
|
+
else:
|
74
|
+
child.add(f'[error]{message_fn(value)}[/error]')
|
75
|
+
|
76
|
+
print_section(sections, tree)
|
77
|
+
|
78
|
+
if tree.children:
|
79
|
+
console.console.rule('Issues', style='error')
|
80
|
+
for child in tree.children:
|
81
|
+
console.console.print(child)
|
82
|
+
|
83
|
+
def print_detailed_report(self):
|
84
|
+
self._print_report_by(
|
85
|
+
self.get_detailed_sections, lambda issue: issue.get_detailed_message()
|
86
|
+
)
|
87
|
+
|
88
|
+
def print_overview_report(self):
|
89
|
+
self._print_report_by(
|
90
|
+
self.get_overview_sections, lambda issue: issue.get_overview_message()
|
91
|
+
)
|
92
|
+
|
93
|
+
|
94
|
+
issue_stack_var = contextvars.ContextVar('issue_stack', default=[IssueAccumulator()])
|
95
|
+
issue_level_var = contextvars.ContextVar('issue_level', default=IssueLevel.DETAILED)
|
96
|
+
|
97
|
+
|
98
|
+
def get_issue_stack() -> list[IssueAccumulator]:
|
99
|
+
return issue_stack_var.get()
|
100
|
+
|
101
|
+
|
102
|
+
def push_issue_accumulator():
|
103
|
+
issue_stack_var.set(get_issue_stack() + [IssueAccumulator()])
|
104
|
+
|
105
|
+
|
106
|
+
def pop_issue_accumulator():
|
107
|
+
issue_stack_var.set(get_issue_stack()[:-1])
|
108
|
+
|
109
|
+
|
110
|
+
def get_issue_accumulator() -> IssueAccumulator:
|
111
|
+
return get_issue_stack()[-1]
|
112
|
+
|
113
|
+
|
114
|
+
def add_issue(issue: Issue):
|
115
|
+
for acc in get_issue_stack():
|
116
|
+
acc.add_issue(issue)
|
117
|
+
|
118
|
+
|
119
|
+
def print_current_report():
|
120
|
+
acc = get_issue_accumulator()
|
121
|
+
if issue_level_var.get() == IssueLevel.OVERVIEW:
|
122
|
+
acc.print_overview_report()
|
123
|
+
else:
|
124
|
+
acc.print_detailed_report()
|
rbx/box/schema.py
CHANGED
@@ -3,13 +3,14 @@ from __future__ import annotations
|
|
3
3
|
import os
|
4
4
|
import pathlib
|
5
5
|
import re
|
6
|
+
import typing
|
6
7
|
from typing import Annotated, Any, Dict, List, Optional
|
7
8
|
|
8
9
|
from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
|
9
10
|
from pydantic_core import PydanticCustomError
|
10
11
|
|
11
12
|
from rbx.autoenum import AutoEnum, alias
|
12
|
-
from rbx.box.fields import NameField, Primitive, expand_vars
|
13
|
+
from rbx.box.fields import NameField, Primitive, RecVars, Vars, expand_vars
|
13
14
|
from rbx.box.statements.expander import expand_statements
|
14
15
|
from rbx.box.statements.schema import Statement
|
15
16
|
from rbx.grading.steps import Outcome
|
@@ -52,7 +53,7 @@ def convert_to_primitive(value: Any) -> Primitive:
|
|
52
53
|
|
53
54
|
def expand_any_vars(vars: Dict[str, Any]) -> Dict[str, Primitive]:
|
54
55
|
converted_vars = {key: convert_to_primitive(value) for key, value in vars.items()}
|
55
|
-
return expand_vars(converted_vars)
|
56
|
+
return expand_vars(typing.cast(RecVars, converted_vars))
|
56
57
|
|
57
58
|
|
58
59
|
def is_unique_by_name(statements: List['Statement']) -> List['Statement']:
|
@@ -330,6 +331,26 @@ problems that have points.
|
|
330
331
|
""",
|
331
332
|
)
|
332
333
|
|
334
|
+
model_solution: Optional[CodeItem] = Field(
|
335
|
+
default=None,
|
336
|
+
description="""
|
337
|
+
The solution to be used to generate outputs for this testgroup.
|
338
|
+
|
339
|
+
Can only be set for the "samples" testgroup.
|
340
|
+
""",
|
341
|
+
)
|
342
|
+
|
343
|
+
@model_validator(mode='after')
|
344
|
+
def check_model_solution_for_samples(self):
|
345
|
+
if self.name == 'samples':
|
346
|
+
return self
|
347
|
+
if self.model_solution is not None:
|
348
|
+
raise PydanticCustomError(
|
349
|
+
'MODEL_SOLUTION_NOT_ALLOWED',
|
350
|
+
'Model solution can only be set for the "samples" testgroup.',
|
351
|
+
)
|
352
|
+
return self
|
353
|
+
|
333
354
|
|
334
355
|
class Generator(CodeItem):
|
335
356
|
model_config = ConfigDict(extra='forbid')
|
@@ -419,6 +440,68 @@ class UnitTests(BaseModel):
|
|
419
440
|
)
|
420
441
|
|
421
442
|
|
443
|
+
class LimitsProfile(BaseModel):
|
444
|
+
model_config = ConfigDict(extra='forbid')
|
445
|
+
|
446
|
+
inheritFromPackage: bool = Field(
|
447
|
+
default=False,
|
448
|
+
description="""
|
449
|
+
Whether to inherit limits from the package.
|
450
|
+
""",
|
451
|
+
)
|
452
|
+
|
453
|
+
timeLimit: Optional[int] = Field(
|
454
|
+
default=None, description='Time limit of the problem, in milliseconds.'
|
455
|
+
)
|
456
|
+
|
457
|
+
memoryLimit: Optional[int] = Field(
|
458
|
+
default=None, description='Memory limit of the problem, in MB.'
|
459
|
+
)
|
460
|
+
|
461
|
+
outputLimit: Optional[int] = Field(
|
462
|
+
default=None, description='Output limit of the problem, in KB.'
|
463
|
+
)
|
464
|
+
|
465
|
+
modifiers: Dict[str, LimitModifiers] = Field(
|
466
|
+
default={},
|
467
|
+
description="""
|
468
|
+
Limit modifiers that can be specified per language.
|
469
|
+
""",
|
470
|
+
)
|
471
|
+
|
472
|
+
formula: Optional[str] = Field(
|
473
|
+
default=None,
|
474
|
+
description="""
|
475
|
+
A formula to estimate the time limit for the problem.
|
476
|
+
""",
|
477
|
+
)
|
478
|
+
|
479
|
+
def timelimit_for_language(self, language: Optional[str] = None) -> int:
|
480
|
+
assert self.timeLimit is not None
|
481
|
+
res = self.timeLimit
|
482
|
+
if language is not None and language in self.modifiers:
|
483
|
+
modifier = self.modifiers[language]
|
484
|
+
if modifier.time is not None:
|
485
|
+
res = modifier.time
|
486
|
+
if modifier.timeMultiplier is not None:
|
487
|
+
res = int(res * float(modifier.timeMultiplier))
|
488
|
+
if 'RBX_TIME_MULTIPLIER' in os.environ:
|
489
|
+
res = int(res * float(os.environ['RBX_TIME_MULTIPLIER']))
|
490
|
+
return res
|
491
|
+
|
492
|
+
def memorylimit_for_language(self, language: Optional[str] = None) -> int:
|
493
|
+
assert self.memoryLimit is not None
|
494
|
+
res = self.memoryLimit
|
495
|
+
if language is None:
|
496
|
+
return res
|
497
|
+
if language not in self.modifiers:
|
498
|
+
return res
|
499
|
+
modifier = self.modifiers[language]
|
500
|
+
if modifier.memory is not None:
|
501
|
+
return modifier.memory
|
502
|
+
return res
|
503
|
+
|
504
|
+
|
422
505
|
class Package(BaseModel):
|
423
506
|
model_config = ConfigDict(extra='forbid')
|
424
507
|
|
@@ -486,7 +569,7 @@ that is correct and used as reference -- and should have the `accepted` outcome.
|
|
486
569
|
# Vars to be re-used across the package.
|
487
570
|
# - It will be passed as --key=value arguments to the validator.
|
488
571
|
# - It will be available as \VAR{key} variables in the rbx statement.
|
489
|
-
vars:
|
572
|
+
vars: RecVars = Field(
|
490
573
|
default={}, description='Variables to be re-used across the package.'
|
491
574
|
)
|
492
575
|
|
@@ -500,32 +583,9 @@ that is correct and used as reference -- and should have the `accepted` outcome.
|
|
500
583
|
return expand_statements(self.statements)
|
501
584
|
|
502
585
|
@property
|
503
|
-
def expanded_vars(self) ->
|
586
|
+
def expanded_vars(self) -> Vars:
|
504
587
|
return expand_vars(self.vars)
|
505
588
|
|
506
|
-
def timelimit_for_language(self, language: Optional[str]) -> int:
|
507
|
-
res = self.timeLimit
|
508
|
-
if language is not None and language in self.modifiers:
|
509
|
-
modifier = self.modifiers[language]
|
510
|
-
if modifier.time is not None:
|
511
|
-
res = modifier.time
|
512
|
-
if modifier.timeMultiplier is not None:
|
513
|
-
res = int(res * float(modifier.timeMultiplier))
|
514
|
-
if 'RBX_TIME_MULTIPLIER' in os.environ:
|
515
|
-
res = int(res * float(os.environ['RBX_TIME_MULTIPLIER']))
|
516
|
-
return res
|
517
|
-
|
518
|
-
def memorylimit_for_language(self, language: Optional[str]) -> int:
|
519
|
-
res = self.memoryLimit
|
520
|
-
if language is None:
|
521
|
-
return res
|
522
|
-
if language not in self.modifiers:
|
523
|
-
return res
|
524
|
-
modifier = self.modifiers[language]
|
525
|
-
if modifier.memory is not None:
|
526
|
-
return modifier.memory
|
527
|
-
return res
|
528
|
-
|
529
589
|
@model_validator(mode='after')
|
530
590
|
def check_first_solution_is_main_if_there_is_ac(self):
|
531
591
|
if all(sol.outcome != Outcome.ACCEPTED for sol in self.solutions):
|
rbx/box/solutions.py
CHANGED
@@ -5,7 +5,7 @@ import dataclasses
|
|
5
5
|
import pathlib
|
6
6
|
import shutil
|
7
7
|
from collections.abc import Iterator
|
8
|
-
from typing import
|
8
|
+
from typing import Dict, Iterable, List, Optional, Set, Tuple
|
9
9
|
|
10
10
|
import rich
|
11
11
|
import rich.live
|
@@ -17,7 +17,7 @@ from ordered_set import OrderedSet
|
|
17
17
|
from pydantic import BaseModel
|
18
18
|
|
19
19
|
from rbx import console, utils
|
20
|
-
from rbx.box import checkers,
|
20
|
+
from rbx.box import checkers, code, limits_info, package, remote, state
|
21
21
|
from rbx.box.code import (
|
22
22
|
SanitizationLevel,
|
23
23
|
compile_item,
|
@@ -34,6 +34,7 @@ from rbx.box.generators import (
|
|
34
34
|
generate_output_for_testcase,
|
35
35
|
generate_standalone,
|
36
36
|
)
|
37
|
+
from rbx.box.sanitizers import issue_stack
|
37
38
|
from rbx.box.schema import (
|
38
39
|
ExpectedOutcome,
|
39
40
|
GeneratorCall,
|
@@ -92,6 +93,20 @@ class SolutionReportSkeleton(BaseModel):
|
|
92
93
|
verification: VerificationLevel
|
93
94
|
capture_pipes: bool = False
|
94
95
|
|
96
|
+
def get_solution_limits(self, solution: Solution) -> Limits:
|
97
|
+
lang = code.find_language_name(solution)
|
98
|
+
if lang is None:
|
99
|
+
return limits_info.get_package_limits(self.verification)
|
100
|
+
return self.limits[lang]
|
101
|
+
|
102
|
+
def get_solution_limits_from_disk(self, solution: Solution) -> Limits:
|
103
|
+
lang = code.find_language_name(solution)
|
104
|
+
return limits_info.get_limits(
|
105
|
+
language=lang,
|
106
|
+
profile=self.get_solution_limits(solution).profile,
|
107
|
+
verification=self.verification,
|
108
|
+
)
|
109
|
+
|
95
110
|
def find_group_skeleton(self, group_name: str) -> Optional[GroupSkeleton]:
|
96
111
|
groups = [group for group in self.groups if group.name == group_name]
|
97
112
|
if not groups:
|
@@ -131,6 +146,17 @@ class RunSolutionResult:
|
|
131
146
|
return self.skeleton.empty_structured_evaluation()
|
132
147
|
|
133
148
|
|
149
|
+
class FailedSolutionIssue(issue_stack.Issue):
|
150
|
+
def __init__(self, solution: Solution):
|
151
|
+
self.solution = solution
|
152
|
+
|
153
|
+
def get_detailed_section(self) -> Tuple[str, ...]:
|
154
|
+
return ('solutions',)
|
155
|
+
|
156
|
+
def get_detailed_message(self) -> str:
|
157
|
+
return f'[item]{href(self.solution.path)}[/item] has an unexpected outcome.'
|
158
|
+
|
159
|
+
|
134
160
|
def is_fast(solution: Solution) -> bool:
|
135
161
|
# If solution has TLE tag, it is considered slow.
|
136
162
|
return not solution.outcome.is_slow()
|
@@ -535,11 +561,13 @@ async def _generate_testcase_interactively(
|
|
535
561
|
if progress:
|
536
562
|
progress.update('Generating output for test...')
|
537
563
|
# TODO: Add stderr path
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
564
|
+
if main_solution is not None:
|
565
|
+
await generate_output_for_testcase(
|
566
|
+
main_solution,
|
567
|
+
main_solution_digest,
|
568
|
+
testcase,
|
569
|
+
interactor_digest=interactor_digest,
|
570
|
+
)
|
543
571
|
|
544
572
|
if check and testcase.outputPath is not None and not testcase.outputPath.is_file():
|
545
573
|
# Output was not created, throw an error.
|
@@ -882,10 +910,10 @@ def get_evals_formatted_time(evals: List[Evaluation]) -> str:
|
|
882
910
|
|
883
911
|
|
884
912
|
def get_capped_evals_formatted_time(
|
885
|
-
|
913
|
+
limits: Limits,
|
914
|
+
evals: List[Evaluation],
|
915
|
+
verification: VerificationLevel,
|
886
916
|
) -> str:
|
887
|
-
pkg = package.find_problem_package_or_die()
|
888
|
-
|
889
917
|
max_time = _get_evals_time_in_ms(evals)
|
890
918
|
has_tle = any(eval.result.outcome.is_slow() for eval in evals)
|
891
919
|
has_ile = any(
|
@@ -902,13 +930,13 @@ def get_capped_evals_formatted_time(
|
|
902
930
|
if timelimits:
|
903
931
|
tl = min(timelimits)
|
904
932
|
if tl is None:
|
905
|
-
tl =
|
933
|
+
tl = limits.time
|
906
934
|
|
907
|
-
if verification.value >= VerificationLevel.FULL.value:
|
935
|
+
if tl is not None and verification.value >= VerificationLevel.FULL.value:
|
908
936
|
# Using double TL for verification.
|
909
937
|
tl = tl * 2
|
910
938
|
|
911
|
-
if has_tle and max_time >= tl or has_ile:
|
939
|
+
if tl is not None and has_tle and max_time >= tl or has_ile:
|
912
940
|
return f'>{tl} ms'
|
913
941
|
return f'{max_time} ms'
|
914
942
|
|
@@ -930,6 +958,7 @@ def get_truncated_message(message: str, max_length: int = 100) -> str:
|
|
930
958
|
|
931
959
|
class SolutionOutcomeReport(BaseModel):
|
932
960
|
solution: Solution
|
961
|
+
limits: Limits
|
933
962
|
evals: List[Evaluation]
|
934
963
|
ok: bool
|
935
964
|
message: Optional[Tuple[TestcaseEntry, str]]
|
@@ -972,7 +1001,7 @@ class SolutionOutcomeReport(BaseModel):
|
|
972
1001
|
|
973
1002
|
def get_outcome_markup(self, print_message: bool = True) -> str:
|
974
1003
|
res = self.get_verdict_markup_with_warnings()
|
975
|
-
res += f'\nTime: {get_capped_evals_formatted_time(self.
|
1004
|
+
res += f'\nTime: {get_capped_evals_formatted_time(self.limits, self.evals, self.verification)}'
|
976
1005
|
res += f'\nMemory: {get_evals_formatted_memory(self.evals)}'
|
977
1006
|
if print_message and self.message is not None:
|
978
1007
|
tc, msg = self.message
|
@@ -989,8 +1018,6 @@ def get_solution_outcome_report(
|
|
989
1018
|
verification: VerificationLevel = VerificationLevel.NONE,
|
990
1019
|
subset: bool = False,
|
991
1020
|
) -> SolutionOutcomeReport:
|
992
|
-
pkg = package.find_problem_package_or_die()
|
993
|
-
|
994
1021
|
has_plain_tle = False
|
995
1022
|
all_verdicts = set()
|
996
1023
|
bad_verdicts = set()
|
@@ -1051,6 +1078,7 @@ def get_solution_outcome_report(
|
|
1051
1078
|
|
1052
1079
|
evals_time = _get_evals_time_in_ms(evals)
|
1053
1080
|
expected_outcome_is_tle = solution.outcome.matches_tle_and_is_incorrect()
|
1081
|
+
limits = skeleton.get_solution_limits(solution)
|
1054
1082
|
if (
|
1055
1083
|
# Running verification with double TL.
|
1056
1084
|
verification.value >= VerificationLevel.FULL.value
|
@@ -1061,7 +1089,8 @@ def get_solution_outcome_report(
|
|
1061
1089
|
# A TLE has happened.
|
1062
1090
|
and Outcome.TIME_LIMIT_EXCEEDED in matched_bad_verdicts
|
1063
1091
|
# The solution runs in double TL.
|
1064
|
-
and
|
1092
|
+
and limits.time is not None
|
1093
|
+
and evals_time < limits.time * 2
|
1065
1094
|
):
|
1066
1095
|
other_verdicts = (bad_verdicts | no_tle_bad_verdicts) - {
|
1067
1096
|
Outcome.TIME_LIMIT_EXCEEDED
|
@@ -1078,6 +1107,7 @@ def get_solution_outcome_report(
|
|
1078
1107
|
|
1079
1108
|
return SolutionOutcomeReport(
|
1080
1109
|
solution=solution,
|
1110
|
+
limits=skeleton.get_solution_limits(solution),
|
1081
1111
|
evals=evals,
|
1082
1112
|
ok=not has_failed,
|
1083
1113
|
message=message,
|
@@ -1102,11 +1132,13 @@ def _print_solution_outcome(
|
|
1102
1132
|
report = get_solution_outcome_report(
|
1103
1133
|
solution, skeleton, evals, verification, subset
|
1104
1134
|
)
|
1135
|
+
if not report.ok:
|
1136
|
+
issue_stack.add_issue(FailedSolutionIssue(solution))
|
1105
1137
|
console.print(report.get_outcome_markup(print_message))
|
1106
1138
|
return report.ok
|
1107
1139
|
|
1108
1140
|
|
1109
|
-
def
|
1141
|
+
def consume_and_key_evaluation_items(
|
1110
1142
|
items: Iterable[EvaluationItem],
|
1111
1143
|
skeleton: SolutionReportSkeleton,
|
1112
1144
|
) -> StructuredEvaluation:
|
@@ -1169,9 +1201,7 @@ async def _print_timing(
|
|
1169
1201
|
console: rich.console.Console,
|
1170
1202
|
skeleton: SolutionReportSkeleton,
|
1171
1203
|
evaluations: StructuredEvaluation,
|
1172
|
-
verification: VerificationLevel,
|
1173
1204
|
):
|
1174
|
-
pkg = package.find_problem_package_or_die()
|
1175
1205
|
summary = TimingSummary()
|
1176
1206
|
summary_per_language = collections.defaultdict(TimingSummary)
|
1177
1207
|
tls_per_language = {}
|
@@ -1196,8 +1226,12 @@ async def _print_timing(
|
|
1196
1226
|
if solution_tls:
|
1197
1227
|
solution_tl = min(solution_tls)
|
1198
1228
|
else:
|
1199
|
-
|
1200
|
-
if
|
1229
|
+
limits = skeleton.get_solution_limits(solution)
|
1230
|
+
if limits.time is None:
|
1231
|
+
limits = skeleton.get_solution_limits_from_disk(solution)
|
1232
|
+
assert limits.time is not None
|
1233
|
+
solution_tl = limits.time
|
1234
|
+
if limits.isDoubleTL:
|
1201
1235
|
solution_tl = solution_tl * 2
|
1202
1236
|
all_tls.add(solution_tl)
|
1203
1237
|
for eval in all_evals:
|
@@ -1306,7 +1340,8 @@ async def _render_detailed_group_table(
|
|
1306
1340
|
evals_per_solution[str(solution.path)].append(eval)
|
1307
1341
|
|
1308
1342
|
verdict = get_testcase_markup_verdict(eval)
|
1309
|
-
|
1343
|
+
limits = skeleton.get_solution_limits(solution)
|
1344
|
+
time = get_capped_evals_formatted_time(limits, [eval], verification)
|
1310
1345
|
memory = get_evals_formatted_memory([eval])
|
1311
1346
|
full_item = (f'[info]#{tc}[/info]', verdict, time, '/', memory, '')
|
1312
1347
|
if eval.result.sanitizer_warnings:
|
@@ -1323,8 +1358,9 @@ async def _render_detailed_group_table(
|
|
1323
1358
|
if not non_null_evals:
|
1324
1359
|
summary_row.append('...')
|
1325
1360
|
continue
|
1361
|
+
limits = skeleton.get_solution_limits(solution)
|
1326
1362
|
formatted_time = get_capped_evals_formatted_time(
|
1327
|
-
|
1363
|
+
limits, non_null_evals, verification
|
1328
1364
|
)
|
1329
1365
|
formatted_memory = get_evals_formatted_memory(non_null_evals)
|
1330
1366
|
worst_outcome = get_worst_outcome(non_null_evals)
|
@@ -1398,7 +1434,9 @@ async def _print_detailed_run_report(
|
|
1398
1434
|
|
1399
1435
|
if timing:
|
1400
1436
|
await _print_timing(
|
1401
|
-
console,
|
1437
|
+
console,
|
1438
|
+
result.skeleton,
|
1439
|
+
structured_evaluations,
|
1402
1440
|
)
|
1403
1441
|
return ok
|
1404
1442
|
|
@@ -1408,7 +1446,10 @@ def _print_limits(limits: Dict[str, Limits]):
|
|
1408
1446
|
'[bold][success]Running with the following limits (per language):[/success][/bold]'
|
1409
1447
|
)
|
1410
1448
|
for lang, limit in limits.items():
|
1411
|
-
|
1449
|
+
extracted_from = ' (extracted from package)'
|
1450
|
+
if limit.profile:
|
1451
|
+
extracted_from = f' (extracted from profile [item]{limit.profile}[/item])'
|
1452
|
+
console.console.print(f'[bold][status]{lang}[/status][/bold]{extracted_from}')
|
1412
1453
|
time = (
|
1413
1454
|
'<No time limit>' if limit.time is None else get_formatted_time(limit.time)
|
1414
1455
|
)
|
@@ -1435,7 +1476,7 @@ async def print_run_report(
|
|
1435
1476
|
if not skip_printing_limits:
|
1436
1477
|
_print_limits(result.skeleton.limits)
|
1437
1478
|
|
1438
|
-
structured_evaluations =
|
1479
|
+
structured_evaluations = consume_and_key_evaluation_items(
|
1439
1480
|
result.items, result.skeleton
|
1440
1481
|
)
|
1441
1482
|
if detailed:
|
@@ -1455,6 +1496,7 @@ async def print_run_report(
|
|
1455
1496
|
if single_solution:
|
1456
1497
|
console.print()
|
1457
1498
|
solution_evals = []
|
1499
|
+
limits = result.skeleton.get_solution_limits(solution)
|
1458
1500
|
for group in result.skeleton.groups:
|
1459
1501
|
if not single_solution:
|
1460
1502
|
console.print(f'[bold][status]{group.name}[/status][/bold] ', end='')
|
@@ -1469,9 +1511,7 @@ async def print_run_report(
|
|
1469
1511
|
console.print(f'{group.name}/{i}', end='')
|
1470
1512
|
if eval.result.sanitizer_warnings:
|
1471
1513
|
console.print('[warning]*[/warning]', end='')
|
1472
|
-
time = get_capped_evals_formatted_time(
|
1473
|
-
solution, [eval], verification
|
1474
|
-
)
|
1514
|
+
time = get_capped_evals_formatted_time(limits, [eval], verification)
|
1475
1515
|
memory = get_evals_formatted_memory([eval])
|
1476
1516
|
console.print(f' ({time}, {memory})', end='')
|
1477
1517
|
checker_msg = eval.result.message
|
@@ -1493,7 +1533,7 @@ async def print_run_report(
|
|
1493
1533
|
if single_solution:
|
1494
1534
|
console.print(f' [status]{group.name}[/status]', end=' ')
|
1495
1535
|
console.print(
|
1496
|
-
f'({get_capped_evals_formatted_time(
|
1536
|
+
f'({get_capped_evals_formatted_time(limits, group_evals, verification)}, {get_evals_formatted_memory(group_evals)})',
|
1497
1537
|
end='',
|
1498
1538
|
)
|
1499
1539
|
console.print()
|
@@ -1513,92 +1553,9 @@ async def print_run_report(
|
|
1513
1553
|
|
1514
1554
|
if not single_solution:
|
1515
1555
|
await _print_timing(
|
1516
|
-
console,
|
1556
|
+
console,
|
1557
|
+
result.skeleton,
|
1558
|
+
structured_evaluations,
|
1517
1559
|
)
|
1518
1560
|
|
1519
1561
|
return ok
|
1520
|
-
|
1521
|
-
|
1522
|
-
def _step_up(x: Any, step: int) -> int:
|
1523
|
-
x = int(x)
|
1524
|
-
return (x + step - 1) // step * step
|
1525
|
-
|
1526
|
-
|
1527
|
-
def _step_down(x: Any, step: int) -> int:
|
1528
|
-
x = int(x)
|
1529
|
-
return x // step * step
|
1530
|
-
|
1531
|
-
|
1532
|
-
async def estimate_time_limit(
|
1533
|
-
console: rich.console.Console,
|
1534
|
-
result: RunSolutionResult,
|
1535
|
-
) -> Optional[int]:
|
1536
|
-
structured_evaluations = _consume_and_key_evaluation_items(
|
1537
|
-
result.items, result.skeleton
|
1538
|
-
)
|
1539
|
-
|
1540
|
-
timing_per_solution = {}
|
1541
|
-
timing_per_language = {}
|
1542
|
-
|
1543
|
-
if not result.skeleton.solutions:
|
1544
|
-
console.print('[error]No solutions to estimate time limit from.[/error]')
|
1545
|
-
return None
|
1546
|
-
|
1547
|
-
for solution in result.skeleton.solutions:
|
1548
|
-
timings = []
|
1549
|
-
for evals in structured_evaluations[str(solution.path)].values():
|
1550
|
-
for ev in evals:
|
1551
|
-
if ev is None:
|
1552
|
-
continue
|
1553
|
-
ev = await ev()
|
1554
|
-
if ev.log.time is not None:
|
1555
|
-
timings.append(int(ev.log.time * 1000))
|
1556
|
-
|
1557
|
-
if not timings:
|
1558
|
-
console.print(
|
1559
|
-
f'[warning]No timings for solution {href(solution.path)}.[/warning]'
|
1560
|
-
)
|
1561
|
-
continue
|
1562
|
-
|
1563
|
-
timing_per_solution[str(solution.path)] = max(timings)
|
1564
|
-
lang = find_language_name(solution)
|
1565
|
-
if lang not in timing_per_language:
|
1566
|
-
timing_per_language[lang] = 0
|
1567
|
-
timing_per_language[lang] = max(timing_per_language[lang], max(timings))
|
1568
|
-
|
1569
|
-
console.rule('[status]Time estimation[/status]', style='status')
|
1570
|
-
|
1571
|
-
fastest_time = min(timing_per_solution.values())
|
1572
|
-
slowest_time = max(timing_per_solution.values())
|
1573
|
-
|
1574
|
-
console.print(f'Fastest solution: {fastest_time} ms')
|
1575
|
-
console.print(f'Slowest solution: {slowest_time} ms')
|
1576
|
-
|
1577
|
-
if len(timing_per_language) > 0:
|
1578
|
-
timing_language_list = [(t, lang) for lang, t in timing_per_language.items()]
|
1579
|
-
fastest_language_time, fastest_language = min(timing_language_list)
|
1580
|
-
slowest_language_time, slowest_language = max(timing_language_list)
|
1581
|
-
|
1582
|
-
console.print(
|
1583
|
-
f'Fastest language: {fastest_language} ({fastest_language_time} ms)'
|
1584
|
-
)
|
1585
|
-
console.print(
|
1586
|
-
f'Slowest language: {slowest_language} ({slowest_language_time} ms)'
|
1587
|
-
)
|
1588
|
-
|
1589
|
-
env = environment.get_environment()
|
1590
|
-
estimated_tl = int(
|
1591
|
-
eval(
|
1592
|
-
env.timing.formula,
|
1593
|
-
{
|
1594
|
-
'fastest': fastest_time,
|
1595
|
-
'slowest': slowest_time,
|
1596
|
-
'step_up': _step_up,
|
1597
|
-
'step_down': _step_down,
|
1598
|
-
},
|
1599
|
-
)
|
1600
|
-
)
|
1601
|
-
|
1602
|
-
console.print(f'Using formula: {env.timing.formula}')
|
1603
|
-
console.print(f'[success]Estimated time limit:[/success] {estimated_tl} ms')
|
1604
|
-
return estimated_tl
|