rbx.cp 0.5.49__py3-none-any.whl → 0.5.51__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 +12 -0
- rbx/box/code.py +1 -0
- rbx/box/download.py +11 -1
- rbx/box/header.py +73 -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 +18 -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 +64 -27
- rbx/box/testcase_utils.py +27 -0
- rbx/box/testcases/main.py +2 -0
- rbx/box/unit.py +31 -9
- rbx/resources/packagers/boca/checker.sh +5 -0
- rbx/resources/templates/rbx.h +90 -0
- {rbx_cp-0.5.49.dist-info → rbx_cp-0.5.51.dist-info}/METADATA +1 -1
- {rbx_cp-0.5.49.dist-info → rbx_cp-0.5.51.dist-info}/RECORD +24 -20
- {rbx_cp-0.5.49.dist-info → rbx_cp-0.5.51.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.49.dist-info → rbx_cp-0.5.51.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.49.dist-info → rbx_cp-0.5.51.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
@@ -47,7 +47,12 @@ from rbx.box.tasks import (
|
|
47
47
|
run_solution_on_testcase,
|
48
48
|
)
|
49
49
|
from rbx.box.testcase_extractors import extract_generation_testcases
|
50
|
-
from rbx.box.testcase_utils import
|
50
|
+
from rbx.box.testcase_utils import (
|
51
|
+
TestcaseEntry,
|
52
|
+
find_built_testcases,
|
53
|
+
parse_interaction,
|
54
|
+
print_interaction,
|
55
|
+
)
|
51
56
|
from rbx.grading.steps import (
|
52
57
|
Evaluation,
|
53
58
|
Outcome,
|
@@ -323,6 +328,19 @@ def _produce_solution_items(
|
|
323
328
|
return res
|
324
329
|
|
325
330
|
|
331
|
+
def print_best_output(output_files: List[pathlib.Path], empty_warning: bool = False):
|
332
|
+
for output_file in output_files:
|
333
|
+
if not output_file.is_file():
|
334
|
+
continue
|
335
|
+
if output_file.suffix == '.pio':
|
336
|
+
print_interaction(parse_interaction(output_file))
|
337
|
+
else:
|
338
|
+
console.console.print(output_file.read_text())
|
339
|
+
return
|
340
|
+
if empty_warning:
|
341
|
+
console.console.print('[warning]Solution produced no output.[/warning]')
|
342
|
+
|
343
|
+
|
326
344
|
def run_solutions(
|
327
345
|
progress: Optional[StatusProgress] = None,
|
328
346
|
tracked_solutions: Optional[Set[str]] = None,
|
@@ -528,6 +546,7 @@ def _run_interactive_solutions(
|
|
528
546
|
output_dir=output_dir,
|
529
547
|
interactor_digest=interactor_digest,
|
530
548
|
verification=verification,
|
549
|
+
capture_pipes=True,
|
531
550
|
)
|
532
551
|
|
533
552
|
yield EvaluationItem(
|
@@ -589,18 +608,28 @@ async def run_and_print_interactive_solutions(
|
|
589
608
|
)
|
590
609
|
|
591
610
|
stdout_path = eval.log.stdout_absolute_path
|
592
|
-
if print:
|
611
|
+
if print and stdout_path is not None:
|
612
|
+
if pkg.type == TaskType.COMMUNICATION:
|
613
|
+
console.console.rule('Interaction', style='status')
|
614
|
+
output_files = [
|
615
|
+
stdout_path.with_suffix('.pio'),
|
616
|
+
stdout_path.with_suffix('.pout'),
|
617
|
+
]
|
618
|
+
print_best_output(output_files, empty_warning=True)
|
619
|
+
|
593
620
|
console.console.rule('Output', style='status')
|
594
|
-
|
595
|
-
|
596
|
-
and stdout_path is not None
|
597
|
-
and stdout_path.is_file()
|
598
|
-
):
|
599
|
-
console.console.print(stdout_path.read_text())
|
600
|
-
else:
|
601
|
-
console.console.print('[warning]Solution produced no output.[/warning]')
|
621
|
+
output_files = [stdout_path]
|
622
|
+
print_best_output(output_files, empty_warning=True)
|
602
623
|
elif stdout_path is not None:
|
603
|
-
|
624
|
+
if stdout_path.with_suffix('.pout').is_file():
|
625
|
+
stdout_path = stdout_path.with_suffix('.pout')
|
626
|
+
|
627
|
+
if stdout_path.is_file():
|
628
|
+
console.console.print(f'[status]Output:[/status] {stdout_path}')
|
629
|
+
if stdout_path.with_suffix('.pio').is_file():
|
630
|
+
console.console.print(
|
631
|
+
f'[status]Interaction:[/status] {stdout_path.with_suffix(".pio")}'
|
632
|
+
)
|
604
633
|
if eval.log.stderr_absolute_path is not None:
|
605
634
|
console.console.print(
|
606
635
|
f'[status]Stderr:[/status] {eval.log.stderr_absolute_path}'
|
@@ -860,29 +889,37 @@ def _print_solution_header(
|
|
860
889
|
console.print(f'({solution_testdir})')
|
861
890
|
|
862
891
|
|
892
|
+
@dataclasses.dataclass
|
893
|
+
class SolutionTiming:
|
894
|
+
time: int
|
895
|
+
solution: Solution
|
896
|
+
|
897
|
+
|
863
898
|
@dataclasses.dataclass
|
864
899
|
class TimingSummary:
|
865
|
-
slowest_good: Optional[
|
866
|
-
fastest_slow: Optional[
|
900
|
+
slowest_good: Optional[SolutionTiming] = None
|
901
|
+
fastest_slow: Optional[SolutionTiming] = None
|
867
902
|
|
868
|
-
def add_good(self, time: int):
|
869
|
-
if self.slowest_good is None or time > self.slowest_good:
|
870
|
-
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)
|
871
906
|
|
872
|
-
def add_slow(self, time: int):
|
873
|
-
if self.fastest_slow is None or time < self.fastest_slow:
|
874
|
-
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)
|
875
910
|
|
876
911
|
def print(self, console: rich.console.Console, tl: Optional[int] = None):
|
877
912
|
if self.slowest_good is not None:
|
878
913
|
console.print(
|
879
|
-
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]'
|
880
915
|
)
|
881
916
|
if self.fastest_slow is not None:
|
882
|
-
fastest_slow = self.fastest_slow
|
883
|
-
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:
|
884
919
|
fastest_slow = f'>{tl}'
|
885
|
-
console.print(
|
920
|
+
console.print(
|
921
|
+
f'Fastest [error]slow[/error] solution: {fastest_slow} ms, [item]{self.fastest_slow.solution.path}[/item]'
|
922
|
+
)
|
886
923
|
|
887
924
|
|
888
925
|
async def _print_timing(
|
@@ -924,11 +961,11 @@ async def _print_timing(
|
|
924
961
|
|
925
962
|
# Get solution timings.
|
926
963
|
if solution.outcome.match(Outcome.ACCEPTED):
|
927
|
-
summary.add_good(solution_time)
|
928
|
-
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)
|
929
966
|
if solution.outcome.is_slow():
|
930
|
-
summary.add_slow(solution_time)
|
931
|
-
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)
|
932
969
|
|
933
970
|
if summary.slowest_good is None and summary.fastest_slow is None:
|
934
971
|
return
|
rbx/box/testcase_utils.py
CHANGED
@@ -2,6 +2,8 @@ import pathlib
|
|
2
2
|
import shutil
|
3
3
|
from typing import List, Optional, Tuple
|
4
4
|
|
5
|
+
import rich
|
6
|
+
import rich.text
|
5
7
|
import typer
|
6
8
|
from pydantic import BaseModel
|
7
9
|
|
@@ -209,3 +211,28 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
|
209
211
|
prefixes=(interactor_prefix, solution_prefix),
|
210
212
|
entries=entries,
|
211
213
|
)
|
214
|
+
|
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
|
+
|
231
|
+
def print_interaction(interaction: TestcaseInteraction):
|
232
|
+
for entry in interaction.entries:
|
233
|
+
text = rich.text.Text(entry.data)
|
234
|
+
if entry.pipe == 0:
|
235
|
+
text.stylize('status')
|
236
|
+
else:
|
237
|
+
text.stylize('info')
|
238
|
+
console.console.print(text, end='')
|
rbx/box/testcases/main.py
CHANGED
@@ -90,6 +90,7 @@ async def _generate_for_editing(
|
|
90
90
|
|
91
91
|
|
92
92
|
@app.command('view, v', help='View a testcase in your default editor.')
|
93
|
+
@package.within_problem
|
93
94
|
@syncer.sync
|
94
95
|
async def view(
|
95
96
|
tc: Annotated[
|
@@ -126,6 +127,7 @@ async def view(
|
|
126
127
|
|
127
128
|
|
128
129
|
@app.command('info, i', help='Show information about testcases.')
|
130
|
+
@package.within_problem
|
129
131
|
@syncer.sync
|
130
132
|
async def info(
|
131
133
|
pattern: Annotated[
|
rbx/box/unit.py
CHANGED
@@ -29,6 +29,16 @@ class CheckerTestEntry(BaseModel):
|
|
29
29
|
answer: Optional[pathlib.Path] = None
|
30
30
|
outcome: ExpectedOutcome
|
31
31
|
|
32
|
+
def running_tests_formatted_string(self) -> str:
|
33
|
+
res = []
|
34
|
+
if self.input:
|
35
|
+
res.append(f'[item]{self.input}[/item]')
|
36
|
+
if self.output:
|
37
|
+
res.append(f'[item]{self.output}[/item]')
|
38
|
+
if self.answer:
|
39
|
+
res.append(f'[item]{self.answer}[/item]')
|
40
|
+
return ', '.join(res)
|
41
|
+
|
32
42
|
|
33
43
|
def _extract_validator_test_entries(
|
34
44
|
tests: List[ValidatorTest],
|
@@ -91,13 +101,16 @@ async def run_validator_unit_tests(progress: StatusProgress):
|
|
91
101
|
if val is not None:
|
92
102
|
vals.append(val)
|
93
103
|
|
104
|
+
console.console.rule('Validator tests', style='info')
|
105
|
+
if not entries:
|
106
|
+
console.console.print('No validator unit tests found.')
|
107
|
+
return
|
108
|
+
|
94
109
|
compiled_validators = validators.compile_validators(vals, progress=progress)
|
95
110
|
|
96
111
|
if progress:
|
97
112
|
progress.update('Running validator unit tests...')
|
98
113
|
|
99
|
-
console.console.rule('Validator tests', style='info')
|
100
|
-
|
101
114
|
for i, test in enumerate(entries):
|
102
115
|
val = _get_validator_for_test(test)
|
103
116
|
if val is None:
|
@@ -127,7 +140,10 @@ async def run_validator_unit_tests(progress: StatusProgress):
|
|
127
140
|
if info.ok:
|
128
141
|
console.console.print(' [status]Actual[/status] VALID')
|
129
142
|
else:
|
130
|
-
console.console.print(
|
143
|
+
console.console.print(' [status]Actual[/status] INVALID')
|
144
|
+
|
145
|
+
if info.message:
|
146
|
+
console.console.print(f' [status]Message[/status] {info.message}')
|
131
147
|
|
132
148
|
|
133
149
|
async def run_checker_unit_tests(progress: StatusProgress):
|
@@ -141,15 +157,19 @@ async def run_checker_unit_tests(progress: StatusProgress):
|
|
141
157
|
)
|
142
158
|
return
|
143
159
|
|
160
|
+
console.console.rule('Checker tests', style='info')
|
161
|
+
|
162
|
+
entries = _extract_checker_test_entries(pkg.unitTests.checker)
|
163
|
+
if not entries:
|
164
|
+
console.console.print('No checker unit tests found.')
|
165
|
+
return
|
166
|
+
|
144
167
|
compiled_digest = checkers.compile_checker(progress=progress)
|
145
168
|
|
146
169
|
if progress:
|
147
170
|
progress.update('Running checker unit tests...')
|
148
171
|
|
149
|
-
console.console.rule('Checker tests', style='info')
|
150
|
-
|
151
172
|
empty_file = package.get_empty_sentinel_path()
|
152
|
-
entries = _extract_checker_test_entries(pkg.unitTests.checker)
|
153
173
|
|
154
174
|
for i, test in enumerate(entries):
|
155
175
|
result = await checkers.check(
|
@@ -169,13 +189,15 @@ async def run_checker_unit_tests(progress: StatusProgress):
|
|
169
189
|
else '[error]FAIL[/error]'
|
170
190
|
)
|
171
191
|
|
172
|
-
console.console.print(
|
192
|
+
console.console.print(
|
193
|
+
f'{markup} Unit test [item]#{i + 1}[/item] ({test.running_tests_formatted_string()})'
|
194
|
+
)
|
173
195
|
console.console.print(f' [status]Expected[/status] {test.outcome.name}')
|
174
196
|
|
175
197
|
if not test.outcome.match(result.outcome):
|
176
198
|
console.console.print(f' [status]Actual[/status] {result.outcome.name}')
|
177
|
-
|
178
|
-
|
199
|
+
if result.message:
|
200
|
+
console.console.print(f' [status]Message[/status] {result.message}')
|
179
201
|
|
180
202
|
|
181
203
|
@syncer.sync
|
@@ -14,11 +14,16 @@ read -r -d '' TestlibContent <<"EOF"
|
|
14
14
|
{{testlib_content}}
|
15
15
|
EOF
|
16
16
|
|
17
|
+
read -r -d '' RbxHeaderContent <<"EOF"
|
18
|
+
{{rbx_header_content}}
|
19
|
+
EOF
|
20
|
+
|
17
21
|
read -r -d '' CheckerContent <<"EOF"
|
18
22
|
{{checker_content}}
|
19
23
|
EOF
|
20
24
|
|
21
25
|
printf "%s" "${TestlibContent}" >testlib.h
|
26
|
+
printf "%s" "${RbxHeaderContent}" >rbx.h
|
22
27
|
printf "%s" "${CheckerContent}" >$CHECKER_PATH
|
23
28
|
|
24
29
|
checker_hash=($(md5sum $CHECKER_PATH))
|