rbx.cp 0.5.50__py3-none-any.whl → 0.5.52__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/cli.py +17 -4
- rbx/box/code.py +2 -1
- rbx/box/download.py +11 -1
- rbx/box/environment.py +11 -1
- rbx/box/header.py +73 -0
- rbx/box/main.py +4 -0
- rbx/box/naming.py +9 -0
- rbx/box/packaging/boca/packager.py +3 -1
- rbx/box/packaging/contest_main.py +6 -4
- rbx/box/packaging/main.py +15 -1
- rbx/box/packaging/moj/packager.py +6 -1
- rbx/box/packaging/polygon/packager.py +30 -7
- rbx/box/packaging/polygon/polygon_api.py +1327 -0
- rbx/box/packaging/polygon/upload.py +336 -0
- rbx/box/packaging/polygon/xml_schema.py +6 -0
- rbx/box/solutions.py +55 -25
- rbx/box/stresses.py +9 -6
- rbx/box/testcase_utils.py +15 -0
- rbx/box/ui/captured_log.py +9 -5
- rbx/box/ui/css/app.tcss +1 -1
- rbx/box/ui/run.py +19 -18
- rbx/box/unit.py +1 -1
- rbx/resources/packagers/boca/checker.sh +5 -0
- rbx/resources/templates/rbx.h +90 -0
- rbx/testing_utils.py +2 -2
- {rbx_cp-0.5.50.dist-info → rbx_cp-0.5.52.dist-info}/METADATA +3 -1
- {rbx_cp-0.5.50.dist-info → rbx_cp-0.5.52.dist-info}/RECORD +30 -26
- {rbx_cp-0.5.50.dist-info → rbx_cp-0.5.52.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.50.dist-info → rbx_cp-0.5.52.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.50.dist-info → rbx_cp-0.5.52.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,336 @@
|
|
1
|
+
import os
|
2
|
+
import pathlib
|
3
|
+
import tempfile
|
4
|
+
from typing import Any, Dict, Optional
|
5
|
+
|
6
|
+
import rich
|
7
|
+
import rich.progress
|
8
|
+
import typer
|
9
|
+
|
10
|
+
from rbx import console
|
11
|
+
from rbx.box import header, package
|
12
|
+
from rbx.box.generators import get_all_built_testcases
|
13
|
+
from rbx.box.packaging.polygon import polygon_api as api
|
14
|
+
from rbx.box.packaging.polygon.packager import code_to_langs, is_valid_lang_code
|
15
|
+
from rbx.box.schema import CodeItem, ExpectedOutcome, Solution, TaskType, Testcase
|
16
|
+
from rbx.box.statements.build_statements import get_relative_assets
|
17
|
+
from rbx.box.statements.builders import (
|
18
|
+
StatementBlocks,
|
19
|
+
StatementBuilderProblem,
|
20
|
+
render_jinja_blocks,
|
21
|
+
)
|
22
|
+
from rbx.box.statements.schema import Statement, StatementType
|
23
|
+
from rbx.box.testcase_utils import get_alternate_interaction_texts, parse_interaction
|
24
|
+
|
25
|
+
_API_URL = 'https://polygon.codeforces.com/api'
|
26
|
+
|
27
|
+
POLY = api.Polygon(
|
28
|
+
_API_URL,
|
29
|
+
os.environ.get('POLYGON_API_KEY', '').strip(),
|
30
|
+
os.environ.get('POLYGON_API_SECRET', '').strip(),
|
31
|
+
)
|
32
|
+
|
33
|
+
|
34
|
+
def _get_source_type(code: CodeItem):
|
35
|
+
return None
|
36
|
+
|
37
|
+
|
38
|
+
def _get_solution_tag(solution: Solution, is_first: bool = False) -> api.SolutionTag:
|
39
|
+
if solution.outcome == ExpectedOutcome.ACCEPTED:
|
40
|
+
return api.SolutionTag.OK if not is_first else api.SolutionTag.MA
|
41
|
+
if solution.outcome == ExpectedOutcome.ACCEPTED_OR_TLE:
|
42
|
+
return api.SolutionTag.TO
|
43
|
+
if solution.outcome == ExpectedOutcome.WRONG_ANSWER:
|
44
|
+
return api.SolutionTag.WA
|
45
|
+
if solution.outcome == ExpectedOutcome.TIME_LIMIT_EXCEEDED:
|
46
|
+
return api.SolutionTag.TL
|
47
|
+
if solution.outcome == ExpectedOutcome.MEMORY_LIMIT_EXCEEDED:
|
48
|
+
return api.SolutionTag.ML
|
49
|
+
if solution.outcome == ExpectedOutcome.RUNTIME_ERROR:
|
50
|
+
return api.SolutionTag.RE
|
51
|
+
return api.SolutionTag.RJ
|
52
|
+
|
53
|
+
|
54
|
+
def _find_or_create_problem(problem_name: str) -> api.Problem:
|
55
|
+
results = POLY.problems_list(name=problem_name)
|
56
|
+
for result in results:
|
57
|
+
if result.name == problem_name:
|
58
|
+
console.console.print(
|
59
|
+
f'Found already existing problem [item]{problem_name}[/item].'
|
60
|
+
)
|
61
|
+
return result
|
62
|
+
console.console.print(f'Creating new problem [item]{problem_name}[/item].')
|
63
|
+
return POLY.problem_create(problem_name)
|
64
|
+
|
65
|
+
|
66
|
+
def _update_problem_info(problem: api.Problem):
|
67
|
+
pkg = package.find_problem_package_or_die()
|
68
|
+
|
69
|
+
problem.update_info(
|
70
|
+
api.ProblemInfo(
|
71
|
+
interactive=pkg.type == TaskType.COMMUNICATION,
|
72
|
+
time_limit=pkg.timeLimit,
|
73
|
+
memory_limit=pkg.memoryLimit,
|
74
|
+
)
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
def _get_checker_name() -> str:
|
79
|
+
checker = package.get_checker()
|
80
|
+
return checker.path.with_stem('checker').name
|
81
|
+
|
82
|
+
|
83
|
+
def _get_interactor_name() -> str:
|
84
|
+
interactor = package.get_interactor()
|
85
|
+
return interactor.path.with_stem('interactor').name
|
86
|
+
|
87
|
+
|
88
|
+
def _get_validator_name() -> str:
|
89
|
+
validator = package.get_validator()
|
90
|
+
return validator.path.with_stem('validator').name
|
91
|
+
|
92
|
+
|
93
|
+
def _update_rbx_header(problem: api.Problem):
|
94
|
+
console.console.print('Uploading rbx.h...')
|
95
|
+
rbx_header = header.get_header()
|
96
|
+
problem.save_file(
|
97
|
+
type=api.FileType.RESOURCE,
|
98
|
+
name='rbx.h',
|
99
|
+
file=rbx_header.read_bytes(),
|
100
|
+
source_type=None,
|
101
|
+
)
|
102
|
+
|
103
|
+
|
104
|
+
def _update_checker(problem: api.Problem):
|
105
|
+
console.console.print('Uploading checker...')
|
106
|
+
checker = package.get_checker()
|
107
|
+
problem.save_file(
|
108
|
+
type=api.FileType.SOURCE,
|
109
|
+
name=_get_checker_name(),
|
110
|
+
file=checker.path.read_bytes(),
|
111
|
+
source_type=_get_source_type(checker),
|
112
|
+
)
|
113
|
+
|
114
|
+
problem.set_checker(_get_checker_name())
|
115
|
+
|
116
|
+
|
117
|
+
def _update_interactor(problem: api.Problem):
|
118
|
+
console.console.print('Uploading interactor...')
|
119
|
+
interactor = package.get_interactor()
|
120
|
+
problem.save_file(
|
121
|
+
type=api.FileType.SOURCE,
|
122
|
+
name=_get_interactor_name(),
|
123
|
+
file=interactor.path.read_bytes(),
|
124
|
+
source_type=_get_source_type(interactor),
|
125
|
+
)
|
126
|
+
|
127
|
+
problem.set_interactor(_get_interactor_name())
|
128
|
+
|
129
|
+
|
130
|
+
def _upload_validator(problem: api.Problem):
|
131
|
+
console.console.print('Uploading validator...')
|
132
|
+
validator = package.get_validator()
|
133
|
+
problem.save_file(
|
134
|
+
type=api.FileType.SOURCE,
|
135
|
+
name=_get_validator_name(),
|
136
|
+
file=validator.path.read_bytes(),
|
137
|
+
source_type=_get_source_type(validator),
|
138
|
+
)
|
139
|
+
|
140
|
+
problem.set_validator(_get_validator_name())
|
141
|
+
|
142
|
+
|
143
|
+
def _save_skip_coinciding_testcases(problem: api.Problem, *args, **kwargs) -> bool:
|
144
|
+
try:
|
145
|
+
problem.save_test(*args, **kwargs)
|
146
|
+
except api.PolygonRequestFailedException as e:
|
147
|
+
if 'test coincides with' in e.comment.lower():
|
148
|
+
return False
|
149
|
+
raise
|
150
|
+
return True
|
151
|
+
|
152
|
+
|
153
|
+
def _get_test_params_for_statement(
|
154
|
+
testcase: Testcase, is_sample: bool
|
155
|
+
) -> Dict[str, Any]:
|
156
|
+
if not is_sample:
|
157
|
+
return {}
|
158
|
+
res: Dict[str, Any] = {'test_use_in_statements': True}
|
159
|
+
if testcase.outputPath is not None:
|
160
|
+
res['test_output_for_statements'] = testcase.outputPath.read_text()
|
161
|
+
else:
|
162
|
+
return res
|
163
|
+
|
164
|
+
pio_path = testcase.outputPath.with_suffix('.pio')
|
165
|
+
if pio_path.is_file():
|
166
|
+
interaction = parse_interaction(pio_path)
|
167
|
+
res['test_input_for_statements'], res['test_output_for_statements'] = (
|
168
|
+
get_alternate_interaction_texts(interaction)
|
169
|
+
)
|
170
|
+
else:
|
171
|
+
pin_path = testcase.outputPath.with_suffix('.pin')
|
172
|
+
if pin_path.is_file():
|
173
|
+
res['test_input_for_statements'] = pin_path.read_text()
|
174
|
+
pout_path = testcase.outputPath.with_suffix('.pout')
|
175
|
+
if pout_path.is_file():
|
176
|
+
res['test_output_for_statements'] = pout_path.read_text()
|
177
|
+
return res
|
178
|
+
|
179
|
+
|
180
|
+
def _upload_testcases(problem: api.Problem):
|
181
|
+
pkg = package.find_problem_package_or_die()
|
182
|
+
testcases = get_all_built_testcases()
|
183
|
+
i = 0
|
184
|
+
|
185
|
+
with rich.progress.Progress(speed_estimate_period=5) as progress:
|
186
|
+
total_len = 0
|
187
|
+
for group in pkg.testcases:
|
188
|
+
total_len += len(testcases[group.name])
|
189
|
+
task_id = progress.add_task('Uploading testcases...', total=total_len)
|
190
|
+
for group in pkg.testcases:
|
191
|
+
for testcase in testcases[group.name]:
|
192
|
+
is_sample = group.name == 'samples'
|
193
|
+
saved = _save_skip_coinciding_testcases(
|
194
|
+
problem,
|
195
|
+
testset='tests',
|
196
|
+
test_index=i + 1,
|
197
|
+
test_input=testcase.inputPath.read_text(),
|
198
|
+
**_get_test_params_for_statement(testcase, is_sample),
|
199
|
+
)
|
200
|
+
progress.update(task_id, advance=1)
|
201
|
+
if saved:
|
202
|
+
i += 1
|
203
|
+
|
204
|
+
|
205
|
+
def _upload_solutions(problem: api.Problem):
|
206
|
+
console.console.print('Uploading main solution...')
|
207
|
+
pkg = package.find_problem_package_or_die()
|
208
|
+
main_solution = pkg.solutions[0]
|
209
|
+
if main_solution is None or main_solution.outcome != ExpectedOutcome.ACCEPTED:
|
210
|
+
return
|
211
|
+
problem.save_solution(
|
212
|
+
main_solution.path.name,
|
213
|
+
main_solution.path.read_bytes(),
|
214
|
+
source_type=_get_source_type(main_solution),
|
215
|
+
tag=api.SolutionTag.MA,
|
216
|
+
)
|
217
|
+
|
218
|
+
for i, solution in enumerate(pkg.solutions):
|
219
|
+
console.console.print(
|
220
|
+
f'Uploading solution [item]{solution.path.name}[/item] (tag: [item]{_get_solution_tag(solution, is_first=i == 0)}[/item])...'
|
221
|
+
)
|
222
|
+
problem.save_solution(
|
223
|
+
solution.path.name,
|
224
|
+
solution.path.read_bytes(),
|
225
|
+
source_type=_get_source_type(solution),
|
226
|
+
tag=_get_solution_tag(solution, is_first=i == 0),
|
227
|
+
)
|
228
|
+
|
229
|
+
|
230
|
+
def _get_statement_for_language(language: str) -> Optional[Statement]:
|
231
|
+
pkg = package.find_problem_package_or_die()
|
232
|
+
for statement in pkg.statements:
|
233
|
+
if statement.language == language:
|
234
|
+
return statement
|
235
|
+
return None
|
236
|
+
|
237
|
+
|
238
|
+
def _get_statement_blocks(statement: Statement) -> StatementBlocks:
|
239
|
+
# TODO: actually try to convert to rbxTeX
|
240
|
+
assert statement.type == StatementType.rbxTeX
|
241
|
+
builder_problem = StatementBuilderProblem(
|
242
|
+
package=package.find_problem_package_or_die(),
|
243
|
+
statement=statement,
|
244
|
+
)
|
245
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
246
|
+
return render_jinja_blocks(
|
247
|
+
pathlib.Path(temp_dir),
|
248
|
+
statement.path.read_bytes(),
|
249
|
+
**builder_problem.build_inner_jinja_kwargs(),
|
250
|
+
)
|
251
|
+
|
252
|
+
|
253
|
+
def _upload_statement_resources(problem: api.Problem, statement: Statement):
|
254
|
+
assets = get_relative_assets(statement.path, statement.assets)
|
255
|
+
for asset, relative_asset in assets:
|
256
|
+
console.console.print(
|
257
|
+
f'Uploading statement resource [item]{relative_asset}[/item]...'
|
258
|
+
)
|
259
|
+
resource_bytes = asset.read_bytes()
|
260
|
+
if len(resource_bytes) >= 1024 * 1024: # >= 1mb
|
261
|
+
console.console.print(
|
262
|
+
f'[error]Statement resource [item]{relative_asset}[/item] is too large to upload (more than 1MB).[/error]'
|
263
|
+
)
|
264
|
+
raise typer.Exit(1)
|
265
|
+
problem.save_statement_resource(
|
266
|
+
name=str(relative_asset),
|
267
|
+
file=resource_bytes,
|
268
|
+
)
|
269
|
+
|
270
|
+
|
271
|
+
def _upload_statement(problem: api.Problem):
|
272
|
+
pkg = package.find_problem_package_or_die()
|
273
|
+
|
274
|
+
languages = set()
|
275
|
+
for statement in pkg.statements:
|
276
|
+
if not is_valid_lang_code(statement.language):
|
277
|
+
continue
|
278
|
+
languages.add(statement.language)
|
279
|
+
for language in languages:
|
280
|
+
statement = _get_statement_for_language(language)
|
281
|
+
if statement is None:
|
282
|
+
continue
|
283
|
+
if statement.type != StatementType.rbxTeX:
|
284
|
+
continue
|
285
|
+
console.console.print(
|
286
|
+
f'Uploading statement for language [item]{language}[/item] (polygon language: [item]{code_to_langs([language])[0]}[/item])...'
|
287
|
+
)
|
288
|
+
blocks = _get_statement_blocks(statement)
|
289
|
+
polygon_statement = api.Statement(
|
290
|
+
encoding='utf-8',
|
291
|
+
name=statement.title,
|
292
|
+
legend=blocks.blocks.get('legend'),
|
293
|
+
input=blocks.blocks.get('input'),
|
294
|
+
output=blocks.blocks.get('output'),
|
295
|
+
interaction=blocks.blocks.get('interaction'),
|
296
|
+
notes=blocks.blocks.get('notes'),
|
297
|
+
)
|
298
|
+
problem.save_statement(
|
299
|
+
lang=code_to_langs([language])[0], problem_statement=polygon_statement
|
300
|
+
)
|
301
|
+
|
302
|
+
_upload_statement_resources(problem, statement)
|
303
|
+
|
304
|
+
|
305
|
+
def _normalize_problem_name(name: str) -> str:
|
306
|
+
return name.replace(' ', '-').replace('_', '-').lower()
|
307
|
+
|
308
|
+
|
309
|
+
async def upload_problem(name: str):
|
310
|
+
pkg = package.find_problem_package_or_die()
|
311
|
+
name = _normalize_problem_name(name)
|
312
|
+
problem = _find_or_create_problem(name)
|
313
|
+
_update_problem_info(problem)
|
314
|
+
_update_checker(problem)
|
315
|
+
_update_rbx_header(problem)
|
316
|
+
|
317
|
+
if (
|
318
|
+
pkg.type == TaskType.COMMUNICATION
|
319
|
+
and package.get_interactor_or_nil() is not None
|
320
|
+
):
|
321
|
+
_update_interactor(problem)
|
322
|
+
|
323
|
+
# if pkg.validator is not None:
|
324
|
+
# _upload_validator(problem)
|
325
|
+
|
326
|
+
_upload_solutions(problem)
|
327
|
+
_upload_testcases(problem)
|
328
|
+
_upload_statement(problem)
|
329
|
+
|
330
|
+
# Commit.
|
331
|
+
console.console.print('Committing changes...')
|
332
|
+
problem.commit_changes()
|
333
|
+
|
334
|
+
console.console.print(
|
335
|
+
f'[success]Problem [item]{name}[/item] uploaded successfully![/success]'
|
336
|
+
)
|
@@ -65,6 +65,10 @@ class Checker(BaseXmlModel):
|
|
65
65
|
testset: Optional[Testset] = element(default=None)
|
66
66
|
|
67
67
|
|
68
|
+
class Interactor(BaseXmlModel):
|
69
|
+
source: File = element()
|
70
|
+
|
71
|
+
|
68
72
|
class Problem(BaseXmlModel, tag='problem'):
|
69
73
|
names: List[Name] = wrapped('names', element(tag='name'), default_factory=list)
|
70
74
|
|
@@ -84,6 +88,8 @@ class Problem(BaseXmlModel, tag='problem'):
|
|
84
88
|
|
85
89
|
checker: Checker = wrapped('assets', element(tag='checker'))
|
86
90
|
|
91
|
+
interactor: Optional[Interactor] = wrapped('assets', element(tag='interactor'))
|
92
|
+
|
87
93
|
|
88
94
|
class ContestProblem(BaseXmlModel):
|
89
95
|
index: str = attr()
|
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 Dict, Iterable, List, Optional, Set, Tuple
|
8
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
9
9
|
|
10
10
|
import rich
|
11
11
|
import rich.live
|
@@ -16,7 +16,7 @@ import typer
|
|
16
16
|
from pydantic import BaseModel
|
17
17
|
|
18
18
|
from rbx import console, utils
|
19
|
-
from rbx.box import checkers, package
|
19
|
+
from rbx.box import checkers, environment, package
|
20
20
|
from rbx.box.code import (
|
21
21
|
SanitizationLevel,
|
22
22
|
compile_item,
|
@@ -889,29 +889,37 @@ def _print_solution_header(
|
|
889
889
|
console.print(f'({solution_testdir})')
|
890
890
|
|
891
891
|
|
892
|
+
@dataclasses.dataclass
|
893
|
+
class SolutionTiming:
|
894
|
+
time: int
|
895
|
+
solution: Solution
|
896
|
+
|
897
|
+
|
892
898
|
@dataclasses.dataclass
|
893
899
|
class TimingSummary:
|
894
|
-
slowest_good: Optional[
|
895
|
-
fastest_slow: Optional[
|
900
|
+
slowest_good: Optional[SolutionTiming] = None
|
901
|
+
fastest_slow: Optional[SolutionTiming] = None
|
896
902
|
|
897
|
-
def add_good(self, time: int):
|
898
|
-
if self.slowest_good is None or time > self.slowest_good:
|
899
|
-
self.slowest_good = time
|
903
|
+
def add_good(self, time: int, solution: Solution):
|
904
|
+
if self.slowest_good is None or time > self.slowest_good.time:
|
905
|
+
self.slowest_good = SolutionTiming(time, solution)
|
900
906
|
|
901
|
-
def add_slow(self, time: int):
|
902
|
-
if self.fastest_slow is None or time < self.fastest_slow:
|
903
|
-
self.fastest_slow = time
|
907
|
+
def add_slow(self, time: int, solution: Solution):
|
908
|
+
if self.fastest_slow is None or time < self.fastest_slow.time:
|
909
|
+
self.fastest_slow = SolutionTiming(time, solution)
|
904
910
|
|
905
911
|
def print(self, console: rich.console.Console, tl: Optional[int] = None):
|
906
912
|
if self.slowest_good is not None:
|
907
913
|
console.print(
|
908
|
-
f'Slowest [success]OK[/success] solution: {self.slowest_good} ms'
|
914
|
+
f'Slowest [success]OK[/success] solution: {self.slowest_good.time} ms, [item]{self.slowest_good.solution.path}[/item]'
|
909
915
|
)
|
910
916
|
if self.fastest_slow is not None:
|
911
|
-
fastest_slow = self.fastest_slow
|
912
|
-
if tl is not None and self.fastest_slow > tl:
|
917
|
+
fastest_slow = self.fastest_slow.time
|
918
|
+
if tl is not None and self.fastest_slow.time > tl:
|
913
919
|
fastest_slow = f'>{tl}'
|
914
|
-
console.print(
|
920
|
+
console.print(
|
921
|
+
f'Fastest [error]slow[/error] solution: {fastest_slow} ms, [item]{self.fastest_slow.solution.path}[/item]'
|
922
|
+
)
|
915
923
|
|
916
924
|
|
917
925
|
async def _print_timing(
|
@@ -953,11 +961,11 @@ async def _print_timing(
|
|
953
961
|
|
954
962
|
# Get solution timings.
|
955
963
|
if solution.outcome.match(Outcome.ACCEPTED):
|
956
|
-
summary.add_good(solution_time)
|
957
|
-
summary_per_language[solution.language].add_good(solution_time)
|
964
|
+
summary.add_good(solution_time, solution)
|
965
|
+
summary_per_language[solution.language].add_good(solution_time, solution)
|
958
966
|
if solution.outcome.is_slow():
|
959
|
-
summary.add_slow(solution_time)
|
960
|
-
summary_per_language[solution.language].add_slow(solution_time)
|
967
|
+
summary.add_slow(solution_time, solution)
|
968
|
+
summary_per_language[solution.language].add_slow(solution_time, solution)
|
961
969
|
|
962
970
|
if summary.slowest_good is None and summary.fastest_slow is None:
|
963
971
|
return
|
@@ -1236,6 +1244,16 @@ async def print_run_report(
|
|
1236
1244
|
return ok
|
1237
1245
|
|
1238
1246
|
|
1247
|
+
def _step_up(x: Any, step: int) -> int:
|
1248
|
+
x = int(x)
|
1249
|
+
return (x + step - 1) // step * step
|
1250
|
+
|
1251
|
+
|
1252
|
+
def _step_down(x: Any, step: int) -> int:
|
1253
|
+
x = int(x)
|
1254
|
+
return x // step * step
|
1255
|
+
|
1256
|
+
|
1239
1257
|
async def estimate_time_limit(
|
1240
1258
|
console: rich.console.Console,
|
1241
1259
|
result: RunSolutionResult,
|
@@ -1254,12 +1272,12 @@ async def estimate_time_limit(
|
|
1254
1272
|
for solution in result.skeleton.solutions:
|
1255
1273
|
timings = []
|
1256
1274
|
for evals in structured_evaluations[str(solution.path)].values():
|
1257
|
-
for
|
1258
|
-
if
|
1275
|
+
for ev in evals:
|
1276
|
+
if ev is None:
|
1259
1277
|
continue
|
1260
|
-
|
1261
|
-
if
|
1262
|
-
timings.append(int(
|
1278
|
+
ev = await ev()
|
1279
|
+
if ev.log.time is not None:
|
1280
|
+
timings.append(int(ev.log.time * 1000))
|
1263
1281
|
|
1264
1282
|
if not timings:
|
1265
1283
|
console.print(
|
@@ -1293,7 +1311,19 @@ async def estimate_time_limit(
|
|
1293
1311
|
f'Slowest language: {slowest_language} ({slowest_language_time} ms)'
|
1294
1312
|
)
|
1295
1313
|
|
1296
|
-
|
1297
|
-
|
1314
|
+
env = environment.get_environment()
|
1315
|
+
estimated_tl = int(
|
1316
|
+
eval(
|
1317
|
+
env.timing.formula,
|
1318
|
+
{
|
1319
|
+
'fastest': fastest_time,
|
1320
|
+
'slowest': slowest_time,
|
1321
|
+
'step_up': _step_up,
|
1322
|
+
'step_down': _step_down,
|
1323
|
+
},
|
1324
|
+
)
|
1325
|
+
)
|
1298
1326
|
|
1327
|
+
console.print(f'Using formula: {env.timing.formula}')
|
1328
|
+
console.print(f'[success]Estimated time limit:[/success] {estimated_tl} ms')
|
1299
1329
|
return estimated_tl
|
rbx/box/stresses.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
import functools
|
2
1
|
import time
|
3
2
|
from shutil import rmtree
|
4
3
|
from typing import List, Optional
|
5
4
|
|
5
|
+
import async_lru
|
6
6
|
import syncer
|
7
7
|
import typer
|
8
8
|
from pydantic import BaseModel
|
@@ -147,7 +147,7 @@ async def run_stress(
|
|
147
147
|
else None,
|
148
148
|
)
|
149
149
|
|
150
|
-
@
|
150
|
+
@async_lru.alru_cache
|
151
151
|
async def run_solution_fn(
|
152
152
|
solution: str,
|
153
153
|
retry_index: Optional[int] = None,
|
@@ -197,7 +197,7 @@ async def run_stress(
|
|
197
197
|
raise typer.Exit(1)
|
198
198
|
expected_output_path = main_testcase_log.stdout_absolute_path
|
199
199
|
|
200
|
-
@
|
200
|
+
@async_lru.alru_cache
|
201
201
|
async def run_solution_and_checker_fn(
|
202
202
|
call: finder_parser.FinderCall,
|
203
203
|
input_path=input_path,
|
@@ -242,9 +242,12 @@ async def run_stress(
|
|
242
242
|
checker_result=eval.result,
|
243
243
|
)
|
244
244
|
|
245
|
-
|
246
|
-
|
247
|
-
|
245
|
+
@syncer.sync
|
246
|
+
async def run_fn(*args, **kwargs):
|
247
|
+
# Wrap the runner in a syncer.sync to make it work with the finder parser.
|
248
|
+
return await run_solution_and_checker_fn(*args, **kwargs)
|
249
|
+
|
250
|
+
runner = finder_parser.FinderTreeRunner(runner=run_fn)
|
248
251
|
finder_outcome: finder_parser.FinderOutcome = runner.transform(parsed_finder)
|
249
252
|
|
250
253
|
internal_error_results = [
|
rbx/box/testcase_utils.py
CHANGED
@@ -213,6 +213,21 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
|
213
213
|
)
|
214
214
|
|
215
215
|
|
216
|
+
def get_alternate_interaction_texts(
|
217
|
+
interaction: TestcaseInteraction,
|
218
|
+
) -> Tuple[str, str]:
|
219
|
+
interactor_entries = []
|
220
|
+
solution_entries = []
|
221
|
+
for entry in interaction.entries:
|
222
|
+
if entry.pipe == 1:
|
223
|
+
solution_entries.append(entry.data)
|
224
|
+
interactor_entries.extend(['\n'] * entry.data.count('\n'))
|
225
|
+
else:
|
226
|
+
interactor_entries.append(entry.data)
|
227
|
+
solution_entries.extend(['\n'] * entry.data.count('\n'))
|
228
|
+
return ''.join(interactor_entries), ''.join(solution_entries)
|
229
|
+
|
230
|
+
|
216
231
|
def print_interaction(interaction: TestcaseInteraction):
|
217
232
|
for entry in interaction.entries:
|
218
233
|
text = rich.text.Text(entry.data)
|
rbx/box/ui/captured_log.py
CHANGED
@@ -8,7 +8,7 @@ import re
|
|
8
8
|
import signal
|
9
9
|
import struct
|
10
10
|
import termios
|
11
|
-
from typing import Callable, List, Optional
|
11
|
+
from typing import Callable, List, Optional, Tuple
|
12
12
|
|
13
13
|
import pyte
|
14
14
|
import textual
|
@@ -321,10 +321,14 @@ class LogDisplay(ScrollView, can_focus=True):
|
|
321
321
|
loop.remove_reader(pout)
|
322
322
|
event.set()
|
323
323
|
|
324
|
-
async def cleanup():
|
324
|
+
async def cleanup(wait_tp: Optional[Tuple[int, int]] = None):
|
325
|
+
if self.exitcode is not None:
|
326
|
+
return
|
325
327
|
try:
|
326
328
|
loop.remove_reader(pout)
|
327
|
-
|
329
|
+
if wait_tp is None:
|
330
|
+
wait_tp = os.waitpid(pid, os.WNOHANG)
|
331
|
+
_, exitstatus = wait_tp
|
328
332
|
exitcode = os.waitstatus_to_exitcode(exitstatus)
|
329
333
|
self.exitcode = exitcode
|
330
334
|
except ChildProcessError:
|
@@ -352,8 +356,8 @@ class LogDisplay(ScrollView, can_focus=True):
|
|
352
356
|
async def wait():
|
353
357
|
while True:
|
354
358
|
try:
|
355
|
-
if os.waitpid(pid, os.WNOHANG) != (0, 0):
|
356
|
-
await cleanup()
|
359
|
+
if (wait_tp := os.waitpid(pid, os.WNOHANG)) != (0, 0):
|
360
|
+
await cleanup(wait_tp)
|
357
361
|
except ChildProcessError:
|
358
362
|
break
|
359
363
|
await asyncio.sleep(0.5)
|