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.
@@ -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 TestcaseEntry, find_built_testcases
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
- if (
595
- eval.testcase.output is not None
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
- console.console.print(f'[status]Output:[/status] {stdout_path}')
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[int] = None
866
- fastest_slow: Optional[int] = None
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(f'Fastest [error]slow[/error] solution: {fastest_slow} ms')
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(f' [status]Actual[/status] {info.message}')
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(f'{markup} Unit test [item]#{i + 1}[/item]')
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
- if result.message:
178
- console.console.print(f' [status]Message[/status] {result.message}')
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))