rbx.cp 0.5.40__py3-none-any.whl → 0.5.45__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.
Files changed (57) hide show
  1. rbx/box/builder.py +6 -6
  2. rbx/box/checkers.py +100 -25
  3. rbx/box/cli.py +868 -0
  4. rbx/box/code.py +272 -84
  5. rbx/box/contest/statements.py +4 -2
  6. rbx/box/generators.py +55 -49
  7. rbx/box/generators_test.py +7 -7
  8. rbx/box/main.py +1 -868
  9. rbx/box/package.py +57 -2
  10. rbx/box/packaging/boca/packager.py +2 -1
  11. rbx/box/packaging/main.py +17 -9
  12. rbx/box/packaging/moj/packager.py +49 -10
  13. rbx/box/retries.py +5 -5
  14. rbx/box/schema.py +20 -4
  15. rbx/box/solutions.py +46 -108
  16. rbx/box/solutions_test.py +5 -6
  17. rbx/box/state.py +1 -0
  18. rbx/box/statements/build_statements.py +4 -2
  19. rbx/box/stresses.py +23 -12
  20. rbx/box/tasks.py +277 -0
  21. rbx/box/testcase_extractors.py +21 -21
  22. rbx/box/testcases/main.py +19 -14
  23. rbx/box/unit.py +10 -7
  24. rbx/box/validators.py +10 -10
  25. rbx/box/validators_test.py +3 -3
  26. rbx/grading/judge/cacher.py +0 -4
  27. rbx/grading/judge/digester.py +0 -3
  28. rbx/grading/judge/sandbox.py +15 -0
  29. rbx/grading/judge/sandboxes/stupid_sandbox.py +20 -6
  30. rbx/grading/judge/sandboxes/timeit.py +117 -7
  31. rbx/grading/judge/storage.py +0 -4
  32. rbx/grading/steps.py +76 -2
  33. rbx/grading/steps_with_caching.py +45 -3
  34. rbx/grading/steps_with_caching_run_test.py +51 -49
  35. rbx/main.py +0 -4
  36. rbx/resources/packagers/moj/scripts/compare.sh +25 -6
  37. rbx/test.py +6 -4
  38. {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/METADATA +2 -2
  39. {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/RECORD +42 -55
  40. {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/WHEEL +1 -1
  41. rbx/testdata/box1/gen1.cpp +0 -7
  42. rbx/testdata/box1/gen2.cpp +0 -9
  43. rbx/testdata/box1/genScript.py +0 -2
  44. rbx/testdata/box1/hard-tle.sol.cpp +0 -26
  45. rbx/testdata/box1/ole.cpp +0 -17
  46. rbx/testdata/box1/problem.rbx.yml +0 -39
  47. rbx/testdata/box1/re.sol.cpp +0 -23
  48. rbx/testdata/box1/sol.cpp +0 -22
  49. rbx/testdata/box1/tests/1.in +0 -1
  50. rbx/testdata/box1/tle-and-incorrect.sol.cpp +0 -33
  51. rbx/testdata/box1/tle.sol.cpp +0 -35
  52. rbx/testdata/box1/validator.cpp +0 -11
  53. rbx/testdata/box1/wa.sol.cpp +0 -22
  54. rbx/testdata/caching/executable.py +0 -1
  55. rbx/testdata/compatible +0 -0
  56. {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/LICENSE +0 -0
  57. {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/entry_points.txt +0 -0
rbx/box/cli.py ADDED
@@ -0,0 +1,868 @@
1
+ import pathlib
2
+ import shlex
3
+ import shutil
4
+ import sys
5
+ import tempfile
6
+ from typing import Annotated, Optional
7
+
8
+ import rich
9
+ import rich.prompt
10
+ import syncer
11
+ import typer
12
+
13
+ from rbx import annotations, config, console, utils
14
+ from rbx.box import (
15
+ cd,
16
+ compile,
17
+ creation,
18
+ download,
19
+ environment,
20
+ generators,
21
+ package,
22
+ presets,
23
+ setter_config,
24
+ state,
25
+ validators,
26
+ )
27
+ from rbx.box.contest import main as contest
28
+ from rbx.box.contest.contest_package import find_contest_yaml
29
+ from rbx.box.environment import VerificationLevel, get_environment_path
30
+ from rbx.box.packaging import main as packaging
31
+ from rbx.box.schema import CodeItem, ExpectedOutcome, TestcaseGroup
32
+ from rbx.box.solutions import (
33
+ estimate_time_limit,
34
+ get_exact_matching_solutions,
35
+ get_matching_solutions,
36
+ pick_solutions,
37
+ print_run_report,
38
+ run_and_print_interactive_solutions,
39
+ run_solutions,
40
+ )
41
+ from rbx.box.statements import build_statements
42
+ from rbx.box.testcase_utils import TestcaseEntry
43
+ from rbx.box.testcases import main as testcases
44
+
45
+ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
46
+ app.add_typer(
47
+ setter_config.app,
48
+ name='config, cfg',
49
+ cls=annotations.AliasGroup,
50
+ help='Manage setter configuration (sub-command).',
51
+ rich_help_panel='Configuration',
52
+ )
53
+ app.add_typer(
54
+ build_statements.app,
55
+ name='statements, st',
56
+ cls=annotations.AliasGroup,
57
+ help='Manage statements (sub-command).',
58
+ rich_help_panel='Deploying',
59
+ )
60
+ app.add_typer(
61
+ download.app,
62
+ name='download',
63
+ cls=annotations.AliasGroup,
64
+ help='Download an asset from supported repositories (sub-command).',
65
+ rich_help_panel='Management',
66
+ )
67
+ app.add_typer(
68
+ presets.app,
69
+ name='presets',
70
+ cls=annotations.AliasGroup,
71
+ help='Manage presets (sub-command).',
72
+ rich_help_panel='Configuration',
73
+ )
74
+ app.add_typer(
75
+ packaging.app,
76
+ name='package, pkg',
77
+ cls=annotations.AliasGroup,
78
+ help='Build problem packages (sub-command).',
79
+ rich_help_panel='Deploying',
80
+ )
81
+ app.add_typer(
82
+ contest.app,
83
+ name='contest',
84
+ cls=annotations.AliasGroup,
85
+ help='Manage contests (sub-command).',
86
+ rich_help_panel='Management',
87
+ )
88
+ app.add_typer(
89
+ testcases.app,
90
+ name='testcases, tc, t',
91
+ cls=annotations.AliasGroup,
92
+ help='Manage testcases (sub-command).',
93
+ rich_help_panel='Management',
94
+ )
95
+
96
+
97
+ @app.callback()
98
+ def main(
99
+ sanitized: bool = typer.Option(
100
+ False,
101
+ '--sanitized',
102
+ '-s',
103
+ help='Whether to compile and run testlib components with sanitizers enabled. '
104
+ 'If you want to run the solutions with sanitizers enabled, use the "-s" flag in the corresponding run command.',
105
+ ),
106
+ debug_logs: bool = typer.Option(
107
+ False,
108
+ '--debug-logs',
109
+ '--debug',
110
+ '-d',
111
+ help='Whether to save extra debug logs along with the evaluation results.',
112
+ ),
113
+ ):
114
+ state.STATE.run_through_cli = True
115
+ state.STATE.sanitized = sanitized
116
+ if sanitized:
117
+ console.console.print(
118
+ '[warning]Sanitizers are running just for testlib components.\n'
119
+ 'If you want to run the solutions with sanitizers enabled, use the [item]-s[/item] flag in the corresponding run command.[/warning]'
120
+ )
121
+ state.STATE.debug_logs = debug_logs
122
+
123
+
124
+ # @app.command('ui', hidden=True)
125
+ # @package.within_problem
126
+ # def ui():
127
+ # ui_pkg.start()
128
+
129
+
130
+ @app.command(
131
+ 'edit, e',
132
+ rich_help_panel='Configuration',
133
+ help='Open problem.rbx.yml in your default editor.',
134
+ )
135
+ @package.within_problem
136
+ def edit():
137
+ console.console.print('Opening problem definition in editor...')
138
+ # Call this function just to raise exception in case we're no in
139
+ # a problem package.
140
+ package.find_problem()
141
+ config.open_editor(package.find_problem_yaml() or pathlib.Path())
142
+
143
+
144
+ @app.command(
145
+ 'build, b', rich_help_panel='Deploying', help='Build all tests for the problem.'
146
+ )
147
+ @package.within_problem
148
+ @syncer.sync
149
+ async def build(verification: environment.VerificationParam):
150
+ from rbx.box import builder
151
+
152
+ await builder.build(verification=verification)
153
+
154
+
155
+ @app.command(
156
+ 'run, r',
157
+ rich_help_panel='Testing',
158
+ help='Build and run solution(s).',
159
+ )
160
+ @package.within_problem
161
+ @syncer.sync
162
+ async def run(
163
+ verification: environment.VerificationParam,
164
+ solution: Annotated[
165
+ Optional[str],
166
+ typer.Argument(
167
+ help='Path to solution to run. If not specified, will run all solutions.'
168
+ ),
169
+ ] = None,
170
+ outcome: Optional[str] = typer.Option(
171
+ None,
172
+ '--outcome',
173
+ '-o',
174
+ help='Include only solutions whose expected outcomes intersect with this.',
175
+ ),
176
+ check: bool = typer.Option(
177
+ True,
178
+ '--nocheck',
179
+ flag_value=False,
180
+ help='Whether to not build outputs for tests and run checker.',
181
+ ),
182
+ detailed: bool = typer.Option(
183
+ False,
184
+ '--detailed',
185
+ '-d',
186
+ help='Whether to print a detailed view of the tests using tables.',
187
+ ),
188
+ timeit: bool = typer.Option(
189
+ False,
190
+ '--time',
191
+ '-t',
192
+ help='Whether to use estimate a time limit based on accepted solutions.',
193
+ ),
194
+ sanitized: bool = typer.Option(
195
+ False,
196
+ '--sanitized',
197
+ '-s',
198
+ help='Whether to compile the solutions with sanitizers enabled.',
199
+ ),
200
+ choice: bool = typer.Option(
201
+ False,
202
+ '--choice',
203
+ '--choose',
204
+ '-c',
205
+ help='Whether to pick solutions interactively.',
206
+ ),
207
+ ):
208
+ main_solution = package.get_main_solution()
209
+ if check and main_solution is None:
210
+ console.console.print(
211
+ '[warning]No main solution found, running without checkers.[/warning]'
212
+ )
213
+ check = False
214
+
215
+ tracked_solutions = None
216
+ if outcome is not None:
217
+ tracked_solutions = {
218
+ str(solution.path)
219
+ for solution in get_matching_solutions(ExpectedOutcome(outcome))
220
+ }
221
+ if solution:
222
+ tracked_solutions = {solution}
223
+
224
+ if choice:
225
+ tracked_solutions = set(pick_solutions(tracked_solutions))
226
+ if not tracked_solutions:
227
+ console.console.print('[error]No solutions selected. Exiting.[/error]')
228
+ raise typer.Exit(1)
229
+
230
+ from rbx.box import builder
231
+
232
+ if not await builder.build(verification=verification, output=check):
233
+ return
234
+
235
+ if verification <= VerificationLevel.VALIDATE.value:
236
+ console.console.print(
237
+ '[warning]Verification level is set to [item]validate (-v1)[/item], so rbx only build tests and validated them.[/warning]'
238
+ )
239
+ return
240
+
241
+ override_tl = None
242
+ if timeit:
243
+ if sanitized:
244
+ console.console.print(
245
+ '[error]Sanitizers are known to be time-hungry, so they cannot be used for time estimation.\n'
246
+ 'Remove either the [item]-s[/item] flag or the [item]-t[/item] flag to run solutions without sanitizers.[/error]'
247
+ )
248
+ raise typer.Exit(1)
249
+
250
+ # Never use sanitizers for time estimation.
251
+ override_tl = await _time_impl(check=check, detailed=False)
252
+ if override_tl is None:
253
+ raise typer.Exit(1)
254
+
255
+ if sanitized:
256
+ console.console.print(
257
+ '[warning]Sanitizers are running, so the time limit for the problem will be dropped, '
258
+ 'and the environment default time limit will be used instead.[/warning]'
259
+ )
260
+
261
+ if sanitized and tracked_solutions is None:
262
+ console.console.print(
263
+ '[warning]Sanitizers are running, and no solutions were specified to run. Will only run [item]ACCEPTED[/item] solutions.'
264
+ )
265
+ tracked_solutions = {
266
+ str(solution.path)
267
+ for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
268
+ }
269
+
270
+ with utils.StatusProgress('Running solutions...') as s:
271
+ solution_result = run_solutions(
272
+ progress=s,
273
+ tracked_solutions=tracked_solutions,
274
+ check=check,
275
+ verification=VerificationLevel(verification),
276
+ timelimit_override=override_tl,
277
+ sanitized=sanitized,
278
+ )
279
+
280
+ console.console.print()
281
+ console.console.rule('[status]Run report[/status]', style='status')
282
+ await print_run_report(
283
+ solution_result,
284
+ console.console,
285
+ VerificationLevel(verification),
286
+ detailed=detailed,
287
+ skip_printing_limits=sanitized,
288
+ )
289
+
290
+
291
+ async def _time_impl(check: bool, detailed: bool) -> Optional[int]:
292
+ if package.get_main_solution() is None:
293
+ console.console.print(
294
+ '[warning]No main solution found, so cannot estimate a time limit.[/warning]'
295
+ )
296
+ return None
297
+
298
+ verification = VerificationLevel.ALL_SOLUTIONS.value
299
+
300
+ with utils.StatusProgress('Running ACCEPTED solutions...') as s:
301
+ tracked_solutions = {
302
+ str(solution.path)
303
+ for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
304
+ }
305
+ solution_result = run_solutions(
306
+ progress=s,
307
+ tracked_solutions=tracked_solutions,
308
+ check=check,
309
+ verification=VerificationLevel(verification),
310
+ timelimit_override=600, # 10 minute time limit for estimation
311
+ )
312
+
313
+ console.console.print()
314
+ console.console.rule(
315
+ '[status]Run report (for time estimation)[/status]', style='status'
316
+ )
317
+ ok = await print_run_report(
318
+ solution_result,
319
+ console.console,
320
+ VerificationLevel(verification),
321
+ detailed=detailed,
322
+ skip_printing_limits=True,
323
+ )
324
+
325
+ if not ok:
326
+ console.console.print(
327
+ '[error]Failed to run ACCEPTED solutions, so cannot estimate a reliable time limit.[/error]'
328
+ )
329
+ return None
330
+
331
+ console.console.print()
332
+ return await estimate_time_limit(console.console, solution_result)
333
+
334
+
335
+ @app.command(
336
+ 'time, t',
337
+ rich_help_panel='Testing',
338
+ help='Estimate a time limit for the problem based on a time limit formula and timings of accepted solutions.',
339
+ )
340
+ @package.within_problem
341
+ @syncer.sync
342
+ async def time(
343
+ check: bool = typer.Option(
344
+ True,
345
+ '--nocheck',
346
+ flag_value=False,
347
+ help='Whether to not build outputs for tests and run checker.',
348
+ ),
349
+ detailed: bool = typer.Option(
350
+ False,
351
+ '--detailed',
352
+ '-d',
353
+ help='Whether to print a detailed view of the tests using tables.',
354
+ ),
355
+ ):
356
+ main_solution = package.get_main_solution()
357
+ if check and main_solution is None:
358
+ console.console.print(
359
+ '[warning]No main solution found, running without checkers.[/warning]'
360
+ )
361
+ check = False
362
+
363
+ from rbx.box import builder
364
+
365
+ verification = VerificationLevel.ALL_SOLUTIONS.value
366
+ if not await builder.build(verification=verification, output=check):
367
+ return None
368
+
369
+ await _time_impl(check, detailed)
370
+
371
+
372
+ @app.command(
373
+ 'irun, ir',
374
+ rich_help_panel='Testing',
375
+ help='Build and run solution(s) by passing testcases in the CLI.',
376
+ )
377
+ @package.within_problem
378
+ @syncer.sync
379
+ async def irun(
380
+ verification: environment.VerificationParam,
381
+ solution: Annotated[
382
+ Optional[str],
383
+ typer.Argument(
384
+ help='Path to solution to run. If not specified, will run all solutions.'
385
+ ),
386
+ ] = None,
387
+ outcome: Optional[str] = typer.Option(
388
+ None,
389
+ '--outcome',
390
+ '-o',
391
+ help='Include only solutions whose expected outcomes intersect with this.',
392
+ ),
393
+ check: bool = typer.Option(
394
+ True,
395
+ '--nocheck',
396
+ flag_value=False,
397
+ help='Whether to not build outputs for tests and run checker.',
398
+ ),
399
+ generator: Optional[str] = typer.Option(
400
+ None,
401
+ '--generator',
402
+ '-g',
403
+ help='Generator call to use to generate a single test for execution.',
404
+ ),
405
+ testcase: Optional[str] = typer.Option(
406
+ None,
407
+ '--testcase',
408
+ '--test',
409
+ '-tc',
410
+ '-t',
411
+ help='Testcase to run, in the format "[group]/[index]". If not specified, will run interactively.',
412
+ ),
413
+ output: bool = typer.Option(
414
+ False,
415
+ '--output',
416
+ '-o',
417
+ help='Whether to ask user for custom output.',
418
+ ),
419
+ print: bool = typer.Option(
420
+ False, '--print', '-p', help='Whether to print outputs to terminal.'
421
+ ),
422
+ sanitized: bool = typer.Option(
423
+ False,
424
+ '--sanitized',
425
+ '-s',
426
+ help='Whether to compile the solutions with sanitizers enabled.',
427
+ ),
428
+ choice: bool = typer.Option(
429
+ False,
430
+ '--choice',
431
+ '--choose',
432
+ '-c',
433
+ help='Whether to pick solutions interactively.',
434
+ ),
435
+ ):
436
+ if not print:
437
+ console.console.print(
438
+ '[warning]Outputs will be written to files. If you wish to print them to the terminal, use the "-p" parameter.'
439
+ )
440
+ if verification < VerificationLevel.ALL_SOLUTIONS.value:
441
+ console.console.print(
442
+ '[warning]Verification level should be at least [item]all solutions (-v4)[/item] to run solutions interactively.'
443
+ )
444
+ return
445
+
446
+ tracked_solutions = None
447
+ if outcome is not None:
448
+ tracked_solutions = {
449
+ str(solution.path)
450
+ for solution in get_matching_solutions(ExpectedOutcome(outcome))
451
+ }
452
+ if solution:
453
+ tracked_solutions = {solution}
454
+
455
+ if choice:
456
+ tracked_solutions = set(pick_solutions(tracked_solutions))
457
+ if not tracked_solutions:
458
+ console.console.print('[error]No solutions selected. Exiting.[/error]')
459
+ raise typer.Exit(1)
460
+
461
+ if sanitized and tracked_solutions is None:
462
+ console.console.print(
463
+ '[warning]Sanitizers are running, and no solutions were specified to run. Will only run [item]ACCEPTED[/item] solutions.'
464
+ )
465
+ tracked_solutions = {
466
+ str(solution.path)
467
+ for solution in get_exact_matching_solutions(ExpectedOutcome.ACCEPTED)
468
+ }
469
+
470
+ with utils.StatusProgress('Running solutions...') as s:
471
+ await run_and_print_interactive_solutions(
472
+ progress=s,
473
+ tracked_solutions=tracked_solutions,
474
+ check=check,
475
+ verification=VerificationLevel(verification),
476
+ generator=generators.get_call_from_string(generator)
477
+ if generator is not None
478
+ else None,
479
+ testcase_entry=TestcaseEntry.parse(testcase) if testcase else None,
480
+ custom_output=output,
481
+ print=print,
482
+ sanitized=sanitized,
483
+ )
484
+
485
+
486
+ @app.command(
487
+ 'create, c',
488
+ rich_help_panel='Management',
489
+ help='Create a new problem package.',
490
+ )
491
+ def create(
492
+ name: str,
493
+ preset: Annotated[
494
+ Optional[str], typer.Option(help='Preset to use when creating the problem.')
495
+ ] = None,
496
+ ):
497
+ if find_contest_yaml() is not None:
498
+ console.console.print(
499
+ '[error]Cannot [item]rbx create[/item] a problem inside a contest.[/error]'
500
+ )
501
+ console.console.print(
502
+ '[error]Instead, use [item]rbx contest add[/item] to add a problem to a contest.[/error]'
503
+ )
504
+ raise typer.Exit(1)
505
+
506
+ if preset is not None:
507
+ creation.create(name, preset=preset)
508
+ return
509
+ creation.create(name)
510
+
511
+
512
+ @app.command(
513
+ 'stress',
514
+ rich_help_panel='Testing',
515
+ help='Run a stress test.',
516
+ )
517
+ @package.within_problem
518
+ def stress(
519
+ name: Annotated[
520
+ str,
521
+ typer.Argument(
522
+ help='Name of the stress test to run (specified in problem.rbx.yml), '
523
+ 'or the generator to run, in case -g is specified.'
524
+ ),
525
+ ],
526
+ generator_args: Annotated[
527
+ Optional[str],
528
+ typer.Option(
529
+ '--generator',
530
+ '-g',
531
+ help='Run generator [name] with these args.',
532
+ ),
533
+ ] = None,
534
+ finder: Annotated[
535
+ Optional[str],
536
+ typer.Option(
537
+ '--finder',
538
+ '-f',
539
+ help='Run a stress with this finder expression.',
540
+ ),
541
+ ] = None,
542
+ timeout: Annotated[
543
+ int,
544
+ typer.Option(
545
+ '--timeout',
546
+ '--time',
547
+ '-t',
548
+ help='For how many seconds to run the stress test.',
549
+ ),
550
+ ] = 10,
551
+ findings: Annotated[
552
+ int,
553
+ typer.Option('--findings', '-n', help='How many breaking tests to look for.'),
554
+ ] = 1,
555
+ verbose: bool = typer.Option(
556
+ False,
557
+ '-v',
558
+ '--verbose',
559
+ help='Whether to print verbose output for checkers and finders.',
560
+ ),
561
+ sanitized: bool = typer.Option(
562
+ False,
563
+ '--sanitized',
564
+ '-s',
565
+ help='Whether to compile the solutions with sanitizers enabled.',
566
+ ),
567
+ ):
568
+ if finder and not generator_args or generator_args and not finder:
569
+ console.console.print(
570
+ '[error]Options --generator/-g and --finder/-f should be specified together.'
571
+ )
572
+ raise typer.Exit(1)
573
+
574
+ from rbx.box import stresses
575
+
576
+ with utils.StatusProgress('Running stress...') as s:
577
+ report = stresses.run_stress(
578
+ name,
579
+ timeout,
580
+ args=generator_args,
581
+ finder=finder,
582
+ findingsLimit=findings,
583
+ progress=s,
584
+ verbose=verbose,
585
+ sanitized=sanitized,
586
+ )
587
+
588
+ stresses.print_stress_report(report)
589
+
590
+ if not report.findings:
591
+ return
592
+
593
+ # Add found tests.
594
+ res = rich.prompt.Confirm.ask(
595
+ 'Do you want to add the tests that were found to a test group?',
596
+ console=console.console,
597
+ )
598
+ if not res:
599
+ return
600
+ testgroup = None
601
+ while testgroup is None or testgroup:
602
+ groups_by_name = {
603
+ name: group
604
+ for name, group in package.get_test_groups_by_name().items()
605
+ if group.generatorScript is not None
606
+ and group.generatorScript.path.suffix == '.txt'
607
+ }
608
+
609
+ import questionary
610
+
611
+ testgroup = questionary.select(
612
+ 'Choose the testgroup to add the tests to.\nOnly test groups that have a .txt generatorScript are shown below: ',
613
+ choices=list(groups_by_name) + ['(create new script)', '(skip)'],
614
+ ).ask()
615
+
616
+ if testgroup == '(create new script)':
617
+ new_script_name = questionary.text(
618
+ 'Enter the name of the new .txt generatorScript file: '
619
+ ).ask()
620
+ new_script_path = pathlib.Path(new_script_name).with_suffix('.txt')
621
+ new_script_path.parent.mkdir(parents=True, exist_ok=True)
622
+ new_script_path.touch()
623
+
624
+ # Temporarily create a new testgroup with the new script.
625
+ testgroup = new_script_path.stem
626
+ groups_by_name[testgroup] = TestcaseGroup(
627
+ name=testgroup, generatorScript=CodeItem(path=new_script_path)
628
+ )
629
+ ru, problem_yml = package.get_ruyaml()
630
+ if 'testcases' not in problem_yml:
631
+ problem_yml['testcases'] = []
632
+ problem_yml['testcases'].append(
633
+ {
634
+ 'name': testgroup,
635
+ 'generatorScript': new_script_path.name,
636
+ }
637
+ )
638
+ dest = package.find_problem_yaml()
639
+ assert dest is not None
640
+ utils.save_ruyaml(dest, ru, problem_yml)
641
+ package.clear_package_cache()
642
+
643
+ if testgroup not in groups_by_name:
644
+ break
645
+ try:
646
+ subgroup = groups_by_name[testgroup]
647
+ assert subgroup.generatorScript is not None
648
+ generator_script = pathlib.Path(subgroup.generatorScript.path)
649
+
650
+ finding_lines = []
651
+ for finding in report.findings:
652
+ line = finding.generator.name
653
+ if finding.generator.args is not None:
654
+ line = f'{line} {finding.generator.args}'
655
+ finding_lines.append(line)
656
+
657
+ with generator_script.open('a') as f:
658
+ stress_text = f'# Obtained by running `rbx {shlex.join(sys.argv[1:])}`'
659
+ finding_text = '\n'.join(finding_lines)
660
+ f.write(f'\n{stress_text}\n{finding_text}\n')
661
+
662
+ console.console.print(
663
+ f"Added [item]{len(report.findings)}[/item] tests to test group [item]{testgroup}[/item]'s generatorScript at [item]{subgroup.generatorScript.path}[/item]"
664
+ )
665
+ except typer.Exit:
666
+ continue
667
+ break
668
+
669
+
670
+ @app.command(
671
+ 'compile',
672
+ rich_help_panel='Testing',
673
+ help='Compile an asset given its path.',
674
+ )
675
+ @package.within_problem
676
+ def compile_command(
677
+ path: Annotated[
678
+ Optional[str],
679
+ typer.Argument(help='Path to the asset to compile.'),
680
+ ] = None,
681
+ sanitized: bool = typer.Option(
682
+ False,
683
+ '--sanitized',
684
+ '-s',
685
+ help='Whether to compile the asset with sanitizers enabled.',
686
+ ),
687
+ warnings: bool = typer.Option(
688
+ False,
689
+ '--warnings',
690
+ '-w',
691
+ help='Whether to compile the asset with warnings enabled.',
692
+ ),
693
+ ):
694
+ if path is None:
695
+ import questionary
696
+
697
+ path = questionary.path("What's the path to your asset?").ask()
698
+ if path is None:
699
+ console.console.print('[error]No path specified.[/error]')
700
+ raise typer.Exit(1)
701
+
702
+ compile.any(path, sanitized, warnings)
703
+
704
+
705
+ @app.command(
706
+ 'validate',
707
+ rich_help_panel='Testing',
708
+ help='Run the validator in a one-off fashion, interactively.',
709
+ )
710
+ @package.within_problem
711
+ @syncer.sync
712
+ async def validate(
713
+ path: Annotated[
714
+ Optional[str],
715
+ typer.Option('--path', '-p', help='Path to the testcase to validate.'),
716
+ ] = None,
717
+ ):
718
+ validator_tuple = validators.compile_main_validator()
719
+ if validator_tuple is None:
720
+ console.console.print('[error]No validator found for this problem.[/error]')
721
+ raise typer.Exit(1)
722
+
723
+ validator, validator_digest = validator_tuple
724
+
725
+ input = console.multiline_prompt('Testcase input')
726
+
727
+ if path is None:
728
+ with tempfile.TemporaryDirectory() as tmpdir:
729
+ tmppath = pathlib.Path(tmpdir) / '000.in'
730
+ tmppath.write_text(input)
731
+
732
+ info = await validators.validate_one_off(
733
+ pathlib.Path(tmppath), validator, validator_digest
734
+ )
735
+ else:
736
+ info = await validators.validate_one_off(
737
+ pathlib.Path(path), validator, validator_digest
738
+ )
739
+
740
+ validators.print_validation_report([info])
741
+
742
+
743
+ @app.command(
744
+ 'unit',
745
+ rich_help_panel='Testing',
746
+ help='Run unit tests for the validator and checker.',
747
+ )
748
+ def unit_tests():
749
+ from rbx.box import unit
750
+
751
+ with utils.StatusProgress('Running unit tests...') as s:
752
+ unit.run_unit_tests(s)
753
+
754
+
755
+ @app.command(
756
+ 'environment, env',
757
+ rich_help_panel='Configuration',
758
+ help='Set or show the current box environment.',
759
+ )
760
+ def environment_command(
761
+ env: Annotated[Optional[str], typer.Argument()] = None,
762
+ install_from: Annotated[
763
+ Optional[str],
764
+ typer.Option(
765
+ '--install',
766
+ '-i',
767
+ help='Whether to install this environment from the given file.',
768
+ ),
769
+ ] = None,
770
+ ):
771
+ if env is None:
772
+ cfg = config.get_config()
773
+ console.console.print(f'Current environment: [item]{cfg.boxEnvironment}[/item]')
774
+ console.console.print(
775
+ f'Location: {environment.get_environment_path(cfg.boxEnvironment)}'
776
+ )
777
+ return
778
+ if install_from is not None:
779
+ environment.install_environment(env, pathlib.Path(install_from))
780
+ if not get_environment_path(env).is_file():
781
+ console.console.print(
782
+ f'[error]Environment [item]{env}[/item] does not exist.[/error]'
783
+ )
784
+ raise typer.Exit(1)
785
+
786
+ cfg = config.get_config()
787
+ if env == cfg.boxEnvironment:
788
+ console.console.print(
789
+ f'Environment is already set to [item]{env}[/item].',
790
+ )
791
+ return
792
+ console.console.print(
793
+ f'Changing box environment from [item]{cfg.boxEnvironment}[/item] to [item]{env}[/item]...'
794
+ )
795
+ cfg.boxEnvironment = env
796
+ config.save_config(cfg)
797
+
798
+ # Also clear cache when changing environments.
799
+ clear()
800
+
801
+
802
+ @app.command(
803
+ 'activate',
804
+ rich_help_panel='Configuration',
805
+ help='Activate the environment of the current preset used by the package.',
806
+ )
807
+ @cd.within_closest_package
808
+ def activate():
809
+ preset_lock = presets.get_preset_lock()
810
+ if preset_lock is None:
811
+ console.console.print(
812
+ '[warning]No configured preset to be activated for this package.[/warning]'
813
+ )
814
+ raise typer.Exit(1)
815
+
816
+ preset = presets.get_installed_preset_or_null(preset_lock.preset_name)
817
+ if preset is None:
818
+ if preset_lock.uri is None:
819
+ console.console.print(
820
+ '[error]Preset is not installed. Install it manually, or specify a URI in [item].preset-lock.yml[/item].[/error]'
821
+ )
822
+ raise typer.Exit(1)
823
+ presets.install(preset_lock.uri)
824
+
825
+ preset = presets.get_installed_preset(preset_lock.preset_name)
826
+
827
+ # Install the environment from the preset if it's not already installed.
828
+ presets.optionally_install_environment_from_preset(
829
+ preset, root=presets.get_preset_installation_path(preset_lock.name)
830
+ )
831
+
832
+ # Activate the environment.
833
+ if preset.env is not None:
834
+ environment_command(preset.name)
835
+
836
+ console.console.print(f'[success]Preset [item]{preset.name}[/item] is activated.')
837
+
838
+
839
+ @app.command(
840
+ 'languages',
841
+ rich_help_panel='Configuration',
842
+ help='List the languages available in this environment',
843
+ )
844
+ def languages():
845
+ env = environment.get_environment()
846
+
847
+ console.console.print(
848
+ f'[success]There are [item]{len(env.languages)}[/item] language(s) available.'
849
+ )
850
+
851
+ for language in env.languages:
852
+ console.console.print(
853
+ f'[item]{language.name}[/item], aka [item]{language.readable_name or language.name}[/item]:'
854
+ )
855
+ console.console.print(language)
856
+ console.console.print()
857
+
858
+
859
+ @app.command(
860
+ 'clear, clean',
861
+ rich_help_panel='Management',
862
+ help='Clears cache and build directories.',
863
+ )
864
+ @cd.within_closest_package
865
+ def clear():
866
+ console.console.print('Cleaning cache and build directories...')
867
+ shutil.rmtree('.box', ignore_errors=True)
868
+ shutil.rmtree('build', ignore_errors=True)