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
@@ -3,6 +3,7 @@ import tempfile
3
3
  import typing
4
4
  from typing import Annotated, Dict, List, Optional, Tuple
5
5
 
6
+ import syncer
6
7
  import typer
7
8
 
8
9
  from rbx import annotations, console
@@ -307,7 +308,8 @@ def build_statement(
307
308
 
308
309
  @app.command('build, b', help='Build statements.')
309
310
  @package.within_problem
310
- def build(
311
+ @syncer.sync
312
+ async def build(
311
313
  verification: environment.VerificationParam,
312
314
  languages: Annotated[
313
315
  Optional[List[str]],
@@ -335,7 +337,7 @@ def build(
335
337
  if samples:
336
338
  from rbx.box import builder
337
339
 
338
- if not builder.build(
340
+ if not await builder.build(
339
341
  verification=verification,
340
342
  groups=set(['samples']),
341
343
  output=None,
rbx/box/stresses.py CHANGED
@@ -3,6 +3,7 @@ import time
3
3
  from shutil import rmtree
4
4
  from typing import List, Optional
5
5
 
6
+ import syncer
6
7
  import typer
7
8
  from pydantic import BaseModel
8
9
 
@@ -15,7 +16,7 @@ from rbx.box.generators import (
15
16
  generate_standalone,
16
17
  )
17
18
  from rbx.box.retries import Retrier
18
- from rbx.box.schema import CodeItem, GeneratorCall, Stress, Testcase
19
+ from rbx.box.schema import CodeItem, GeneratorCall, Stress, TaskType, Testcase
19
20
  from rbx.box.solutions import compile_solutions, get_outcome_style_verdict
20
21
  from rbx.box.stressing import finder_parser
21
22
  from rbx.grading.steps import (
@@ -49,7 +50,8 @@ def _compile_finder(finder: CodeItem) -> str:
49
50
  return digest
50
51
 
51
52
 
52
- def run_stress(
53
+ @syncer.sync
54
+ async def run_stress(
53
55
  name: str,
54
56
  timeoutInSeconds: int,
55
57
  finder: Optional[str] = None,
@@ -59,6 +61,13 @@ def run_stress(
59
61
  progress: Optional[StatusProgress] = None,
60
62
  sanitized: bool = False,
61
63
  ) -> StressReport:
64
+ pkg = package.find_problem_package_or_die()
65
+ if pkg.type == TaskType.COMMUNICATION:
66
+ console.console.print(
67
+ '[error]Communication problems do not support stress testing.[/error]'
68
+ )
69
+ raise typer.Exit(1)
70
+
62
71
  if finder:
63
72
  stress = Stress(
64
73
  name=f'{name}',
@@ -128,7 +137,7 @@ def run_stress(
128
137
  input_path.parent.mkdir(parents=True, exist_ok=True)
129
138
 
130
139
  expanded_generator_call = expand_generator_call(stress.generator)
131
- generate_standalone(
140
+ await generate_standalone(
132
141
  GenerationMetadata(
133
142
  generator_call=expanded_generator_call,
134
143
  copied_to=Testcase(inputPath=input_path),
@@ -140,7 +149,7 @@ def run_stress(
140
149
  )
141
150
 
142
151
  @functools.cache
143
- def run_solution_fn(
152
+ async def run_solution_fn(
144
153
  solution: str,
145
154
  retry_index: Optional[int] = None,
146
155
  input_path=input_path,
@@ -150,7 +159,7 @@ def run_stress(
150
159
  output_path = input_path.with_stem(f'{index}').with_suffix('.out')
151
160
  stderr_path = output_path.with_suffix('.err')
152
161
 
153
- run_log = run_item(
162
+ run_log = await run_item(
154
163
  sol,
155
164
  DigestOrSource.create(solutions_digest[sol.path]),
156
165
  stdin=DigestOrSource.create(input_path),
@@ -168,7 +177,7 @@ def run_stress(
168
177
  # Get main solution output.
169
178
  expected_output_path = empty_path
170
179
  if needs_expected_output:
171
- main_testcase_log = run_solution_fn(str(solutions[0].path))
180
+ main_testcase_log = await run_solution_fn(str(solutions[0].path))
172
181
  main_checker_result = checkers.check_with_no_output(main_testcase_log)
173
182
  if main_checker_result.outcome != Outcome.ACCEPTED:
174
183
  console.console.print(
@@ -190,23 +199,23 @@ def run_stress(
190
199
  expected_output_path = main_testcase_log.stdout_absolute_path
191
200
 
192
201
  @functools.cache
193
- def run_solution_and_checker_fn(
202
+ async def run_solution_and_checker_fn(
194
203
  call: finder_parser.FinderCall,
195
204
  input_path=input_path,
196
205
  expected_output_path=expected_output_path,
197
206
  ) -> finder_parser.FinderResult:
198
- def run_fn(retry_index: int) -> Evaluation:
207
+ async def run_fn(retry_index: int) -> Evaluation:
199
208
  solution = call.solution
200
209
  checker = call.checker
201
210
 
202
- testcase_log = run_solution_fn(solution, retry_index=retry_index)
211
+ testcase_log = await run_solution_fn(solution, retry_index=retry_index)
203
212
  assert testcase_log.stdout_absolute_path is not None
204
213
 
205
214
  if checker is None:
206
215
  checker_result = checkers.check_with_no_output(testcase_log)
207
216
  else:
208
217
  checker_digest = finders_digest[checker.path]
209
- checker_result = checkers.check(
218
+ checker_result = await checkers.check(
210
219
  checker_digest,
211
220
  testcase_log,
212
221
  Testcase(inputPath=input_path, outputPath=expected_output_path),
@@ -224,7 +233,7 @@ def run_stress(
224
233
  )
225
234
 
226
235
  retrier = Retrier(is_stress=True)
227
- eval = retrier.repeat(run_fn)
236
+ eval = await retrier.repeat(run_fn)
228
237
 
229
238
  return finder_parser.FinderResult(
230
239
  solution=call.solution,
@@ -234,7 +243,9 @@ def run_stress(
234
243
  checker_result=eval.result,
235
244
  )
236
245
 
237
- runner = finder_parser.FinderTreeRunner(runner=run_solution_and_checker_fn)
246
+ runner = finder_parser.FinderTreeRunner(
247
+ runner=syncer.sync(run_solution_and_checker_fn)
248
+ )
238
249
  finder_outcome: finder_parser.FinderOutcome = runner.transform(parsed_finder)
239
250
 
240
251
  internal_error_results = [
rbx/box/tasks.py ADDED
@@ -0,0 +1,277 @@
1
+ import pathlib
2
+ from typing import Optional
3
+
4
+ from rbx.box import checkers, package, state
5
+ from rbx.box.code import CommunicationItem, run_communication, run_item
6
+ from rbx.box.environment import EnvironmentSandbox, ExecutionConfig, VerificationLevel
7
+ from rbx.box.retries import Retrier
8
+ from rbx.box.schema import Limits, Solution, Testcase
9
+ from rbx.grading.judge.sandbox import SandboxBase
10
+ from rbx.grading.steps import (
11
+ DigestOrDest,
12
+ DigestOrSource,
13
+ Evaluation,
14
+ GradingFileInput,
15
+ GradingFileOutput,
16
+ TestcaseIO,
17
+ TestcaseLog,
18
+ )
19
+ from rbx.utils import model_to_yaml
20
+
21
+
22
+ def get_limits_for_language(
23
+ lang: Optional[str],
24
+ verification: VerificationLevel,
25
+ timelimit_override: Optional[int],
26
+ use_timelimit: bool = True,
27
+ ) -> Limits:
28
+ pkg = package.find_problem_package_or_die()
29
+ time = timelimit_override or pkg.timelimit_for_language(lang)
30
+ isDoubleTL = verification.value >= VerificationLevel.FULL.value
31
+ memory = pkg.memorylimit_for_language(lang)
32
+ return Limits(
33
+ time=time if use_timelimit else None,
34
+ memory=memory,
35
+ output=pkg.outputLimit,
36
+ isDoubleTL=isDoubleTL,
37
+ )
38
+
39
+
40
+ async def run_solution_on_testcase(
41
+ solution: Solution,
42
+ compiled_digest: str,
43
+ checker_digest: Optional[str],
44
+ testcase: Testcase,
45
+ output_dir: pathlib.Path,
46
+ interactor_digest: Optional[str] = None,
47
+ testcase_index: int = 0,
48
+ verification: VerificationLevel = VerificationLevel.NONE,
49
+ timelimit_override: Optional[int] = None,
50
+ use_retries: bool = True,
51
+ use_timelimit: bool = True,
52
+ capture_pipes: bool = False,
53
+ ) -> Evaluation:
54
+ if interactor_digest is not None:
55
+ return await _run_communication_solution_on_testcase(
56
+ solution,
57
+ compiled_digest,
58
+ interactor_digest,
59
+ checker_digest,
60
+ testcase,
61
+ output_dir,
62
+ testcase_index=testcase_index,
63
+ verification=verification,
64
+ timelimit_override=timelimit_override,
65
+ use_retries=use_retries,
66
+ use_timelimit=use_timelimit,
67
+ capture_pipes=capture_pipes,
68
+ )
69
+
70
+ async def run_fn(retry_index: int) -> Evaluation:
71
+ actual_sandbox = package.get_singleton_sandbox()
72
+
73
+ limits = get_limits_for_language(
74
+ solution.language,
75
+ verification,
76
+ timelimit_override,
77
+ use_timelimit=use_timelimit,
78
+ )
79
+ extra_config = _get_execution_config(limits, actual_sandbox)
80
+
81
+ output_path = output_dir / testcase.inputPath.with_suffix('.out').name
82
+ error_path = output_path.with_suffix('.err')
83
+ log_path = output_path.with_suffix('.log')
84
+ output_path.parent.mkdir(parents=True, exist_ok=True)
85
+
86
+ run_log = await run_item(
87
+ solution,
88
+ DigestOrSource.create(compiled_digest),
89
+ stdin=DigestOrSource.create(testcase.inputPath),
90
+ stdout=DigestOrDest.create(output_path),
91
+ stderr=DigestOrDest.create(error_path),
92
+ extra_config=extra_config,
93
+ retry_index=retry_index,
94
+ )
95
+
96
+ if checker_digest is not None:
97
+ checker_result = await checkers.check(
98
+ checker_digest,
99
+ run_log,
100
+ testcase,
101
+ program_output=output_path,
102
+ )
103
+ else:
104
+ checker_result = checkers.check_with_no_output(run_log)
105
+
106
+ eval = Evaluation(
107
+ result=checker_result,
108
+ testcase=TestcaseIO(
109
+ index=testcase_index,
110
+ input=testcase.inputPath,
111
+ output=testcase.outputPath,
112
+ ),
113
+ log=TestcaseLog(
114
+ **(run_log.model_dump() if run_log is not None else {}),
115
+ stdout_absolute_path=output_path.absolute(),
116
+ stderr_absolute_path=error_path.absolute(),
117
+ log_absolute_path=log_path.absolute(),
118
+ ),
119
+ )
120
+
121
+ log_path.write_text(model_to_yaml(eval))
122
+ return eval
123
+
124
+ if not use_retries:
125
+ return await run_fn(0)
126
+
127
+ retrier = Retrier()
128
+ return await retrier.repeat(run_fn)
129
+
130
+
131
+ def _get_execution_config(
132
+ limits: Limits,
133
+ actual_sandbox: SandboxBase,
134
+ ) -> ExecutionConfig:
135
+ sandbox = EnvironmentSandbox()
136
+ sandbox.timeLimit = limits.time
137
+ if limits.isDoubleTL and sandbox.timeLimit is not None:
138
+ # Double TL.
139
+ sandbox.timeLimit = sandbox.timeLimit * 2
140
+ sandbox.wallTimeLimit = sandbox.timeLimit
141
+ if sandbox.timeLimit is not None and actual_sandbox.use_soft_timeout():
142
+ sandbox.wallTimeLimit = sandbox.timeLimit * 2
143
+ sandbox.memoryLimit = limits.memory
144
+ sandbox.fileSizeLimit = limits.output
145
+ return ExecutionConfig(sandbox=sandbox)
146
+
147
+
148
+ async def _run_communication_solution_on_testcase(
149
+ solution: Solution,
150
+ compiled_digest: str,
151
+ interactor_digest: str,
152
+ checker_digest: Optional[str],
153
+ testcase: Testcase,
154
+ output_dir: pathlib.Path,
155
+ testcase_index: int = 0,
156
+ verification: VerificationLevel = VerificationLevel.NONE,
157
+ timelimit_override: Optional[int] = None,
158
+ use_retries: bool = True,
159
+ use_timelimit: bool = True,
160
+ capture_pipes: bool = False,
161
+ ) -> Evaluation:
162
+ capture_pipes = capture_pipes or state.STATE.debug_logs
163
+
164
+ async def run_fn(retry_index: int) -> Evaluation:
165
+ actual_sandbox = package.get_singleton_sandbox()
166
+ interactor_sandbox = package.get_singleton_interactor_sandbox()
167
+
168
+ limits = get_limits_for_language(
169
+ solution.language,
170
+ verification,
171
+ timelimit_override,
172
+ use_timelimit=use_timelimit,
173
+ )
174
+
175
+ extra_config = _get_execution_config(limits, actual_sandbox)
176
+ interactor_extra_config = _get_execution_config(limits, interactor_sandbox)
177
+ if (
178
+ interactor_extra_config.sandbox is not None
179
+ and interactor_extra_config.sandbox.wallTimeLimit is not None
180
+ and extra_config.sandbox is not None
181
+ and extra_config.sandbox.wallTimeLimit is not None
182
+ ):
183
+ interactor_extra_config.sandbox.wallTimeLimit += (
184
+ extra_config.sandbox.wallTimeLimit
185
+ )
186
+ # TODO: maybe combine wall time limits?
187
+
188
+ output_path = output_dir / testcase.inputPath.with_suffix('.out').name
189
+ error_path = output_path.with_suffix('.err')
190
+ log_path = output_path.with_suffix('.log')
191
+ output_path.parent.mkdir(parents=True, exist_ok=True)
192
+
193
+ interactor_capture_path = (
194
+ output_path.with_suffix('.pin') if capture_pipes else None
195
+ )
196
+ interactor_item = CommunicationItem(
197
+ code=package.get_interactor(),
198
+ executable=DigestOrSource.create(interactor_digest),
199
+ stderr=DigestOrDest.create(error_path),
200
+ extra_config=interactor_extra_config,
201
+ extra_args='interactor.in interactor.out',
202
+ inputs=[
203
+ GradingFileInput(
204
+ src=testcase.inputPath,
205
+ dest=pathlib.PosixPath('interactor.in'),
206
+ )
207
+ ],
208
+ outputs=[
209
+ GradingFileOutput(
210
+ src=pathlib.PosixPath('interactor.out'),
211
+ dest=output_path,
212
+ touch=True,
213
+ )
214
+ ],
215
+ capture=DigestOrDest.create(interactor_capture_path)
216
+ if interactor_capture_path
217
+ else None,
218
+ )
219
+ solution_capture_path = (
220
+ output_path.with_suffix('.pout') if capture_pipes else None
221
+ )
222
+ solution_item = CommunicationItem(
223
+ code=solution,
224
+ executable=DigestOrSource.create(compiled_digest),
225
+ extra_config=extra_config,
226
+ capture=DigestOrDest.create(solution_capture_path)
227
+ if solution_capture_path
228
+ else None,
229
+ )
230
+
231
+ merged_capture_path = output_path.with_suffix('.pio') if capture_pipes else None
232
+ interactor_run_log, run_log = await run_communication(
233
+ interactor=interactor_item,
234
+ solution=solution_item,
235
+ retry_index=retry_index,
236
+ merged_capture=merged_capture_path,
237
+ )
238
+
239
+ checker_result = await checkers.check_communication(
240
+ checker_digest,
241
+ run_log,
242
+ interactor_run_log,
243
+ error_path,
244
+ testcase,
245
+ output_path,
246
+ )
247
+
248
+ eval = Evaluation(
249
+ result=checker_result,
250
+ testcase=TestcaseIO(
251
+ index=testcase_index,
252
+ input=testcase.inputPath,
253
+ output=testcase.outputPath,
254
+ ),
255
+ log=TestcaseLog(
256
+ **(run_log.model_dump() if run_log is not None else {}),
257
+ stdout_absolute_path=output_path.absolute(),
258
+ stderr_absolute_path=error_path.absolute(),
259
+ log_absolute_path=log_path.absolute(),
260
+ ),
261
+ )
262
+
263
+ log_path.write_text(model_to_yaml(eval))
264
+
265
+ if interactor_run_log is not None:
266
+ interactor_log_path = output_path.with_suffix('.int.log')
267
+ interactor_log_path.write_text(model_to_yaml(interactor_run_log))
268
+ if run_log is not None:
269
+ solution_log_path = output_path.with_suffix('.sol.log')
270
+ solution_log_path.write_text(model_to_yaml(run_log))
271
+ return eval
272
+
273
+ if not use_retries:
274
+ return await run_fn(0)
275
+
276
+ retrier = Retrier()
277
+ return await retrier.repeat(run_fn)
@@ -30,7 +30,7 @@ def _get_group_output(
30
30
  return group_path / f'{subgroup_prefix}{i:03d}.out'
31
31
 
32
32
 
33
- def _run_generator_script(testcase: TestcaseSubgroup) -> str:
33
+ async def _run_generator_script(testcase: TestcaseSubgroup) -> str:
34
34
  assert testcase.generatorScript is not None
35
35
 
36
36
  cacher = package.get_file_cacher()
@@ -54,7 +54,7 @@ def _run_generator_script(testcase: TestcaseSubgroup) -> str:
54
54
  raise
55
55
 
56
56
  run_stderr = DigestHolder()
57
- run_log = run_item(
57
+ run_log = await run_item(
58
58
  testcase.generatorScript,
59
59
  DigestOrSource.create(compiled_digest),
60
60
  stdout=DigestOrDest.create(script_digest),
@@ -116,7 +116,7 @@ class GenerationTestcaseEntry(BaseModel):
116
116
 
117
117
  class TestcaseVisitor(abc.ABC):
118
118
  @abc.abstractmethod
119
- def visit(self, entry: GenerationTestcaseEntry):
119
+ async def visit(self, entry: GenerationTestcaseEntry):
120
120
  pass
121
121
 
122
122
  def should_visit_group(self, group_name: str) -> bool:
@@ -139,10 +139,10 @@ class TestcaseGroupVisitor(TestcaseVisitor):
139
139
  return self.groups is None or group_name in self.groups
140
140
 
141
141
 
142
- def run_testcase_visitor(visitor: TestcaseVisitor):
142
+ async def run_testcase_visitor(visitor: TestcaseVisitor):
143
143
  pkg = package.find_problem_package_or_die()
144
144
 
145
- def _explore_subgroup(
145
+ async def _explore_subgroup(
146
146
  subgroup: TestcaseSubgroup,
147
147
  subgroup_index: Optional[int],
148
148
  prefix: List[str],
@@ -179,7 +179,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
179
179
  i = 0
180
180
  # Individual testcases.
181
181
  for tc in subgroup.testcases or []:
182
- visitor.visit(
182
+ await visitor.visit(
183
183
  GenerationTestcaseEntry(
184
184
  group_entry=_entry(i),
185
185
  subgroup_entry=_sub_entry(i),
@@ -202,7 +202,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
202
202
  continue
203
203
 
204
204
  tc = Testcase(inputPath=input_path)
205
- visitor.visit(
205
+ await visitor.visit(
206
206
  GenerationTestcaseEntry(
207
207
  group_entry=_entry(i),
208
208
  subgroup_entry=_sub_entry(i),
@@ -218,7 +218,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
218
218
 
219
219
  # Single generators.
220
220
  for generator_call in subgroup.generators:
221
- visitor.visit(
221
+ await visitor.visit(
222
222
  GenerationTestcaseEntry(
223
223
  group_entry=_entry(i),
224
224
  subgroup_entry=_sub_entry(i),
@@ -237,12 +237,12 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
237
237
 
238
238
  # Run generator script.
239
239
  if subgroup.generatorScript is not None:
240
- script = _run_generator_script(subgroup)
240
+ script = await _run_generator_script(subgroup)
241
241
 
242
242
  # Run each line from generator script.
243
243
  for generator_name, args, line_number in _extract_script_lines(script):
244
244
  call = GeneratorCall(name=generator_name, args=args)
245
- visitor.visit(
245
+ await visitor.visit(
246
246
  GenerationTestcaseEntry(
247
247
  group_entry=_entry(i),
248
248
  subgroup_entry=_sub_entry(i),
@@ -269,7 +269,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
269
269
  group_validator = group.validator
270
270
 
271
271
  extra_validators = group.extraValidators
272
- _explore_subgroup(
272
+ await _explore_subgroup(
273
273
  group,
274
274
  0 if group.subgroups else None,
275
275
  [group.name],
@@ -278,7 +278,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
278
278
  )
279
279
 
280
280
  for i, subgroup in enumerate(group.subgroups):
281
- _explore_subgroup(
281
+ await _explore_subgroup(
282
282
  subgroup,
283
283
  i + 1,
284
284
  [group.name, subgroup.name],
@@ -287,7 +287,7 @@ def run_testcase_visitor(visitor: TestcaseVisitor):
287
287
  )
288
288
 
289
289
 
290
- def extract_generation_testcases(
290
+ async def extract_generation_testcases(
291
291
  entries: List[TestcaseEntry],
292
292
  ) -> List[GenerationTestcaseEntry]:
293
293
  # TODO: support subgroups.
@@ -300,30 +300,30 @@ def extract_generation_testcases(
300
300
  def should_visit_group(self, group_name: str) -> bool:
301
301
  return group_name in groups
302
302
 
303
- def visit(self, entry: GenerationTestcaseEntry):
303
+ async def visit(self, entry: GenerationTestcaseEntry):
304
304
  # TODO: support subgroups.
305
305
  if entry.group_entry.key() not in entry_keys:
306
306
  return
307
307
  res.append(entry)
308
308
 
309
- run_testcase_visitor(ExtractGenerationTestcasesVisitor())
309
+ await run_testcase_visitor(ExtractGenerationTestcasesVisitor())
310
310
  return res
311
311
 
312
312
 
313
- def extract_generation_testcases_from_groups(
313
+ async def extract_generation_testcases_from_groups(
314
314
  groups: Optional[Set[str]] = None,
315
315
  ) -> List[GenerationTestcaseEntry]:
316
316
  res: List[GenerationTestcaseEntry] = []
317
317
 
318
318
  class ExtractGenerationTestcasesVisitor(TestcaseGroupVisitor):
319
- def visit(self, entry: GenerationTestcaseEntry):
319
+ async def visit(self, entry: GenerationTestcaseEntry):
320
320
  res.append(entry)
321
321
 
322
- run_testcase_visitor(ExtractGenerationTestcasesVisitor(groups))
322
+ await run_testcase_visitor(ExtractGenerationTestcasesVisitor(groups))
323
323
  return res
324
324
 
325
325
 
326
- def extract_generation_testcases_from_patterns(
326
+ async def extract_generation_testcases_from_patterns(
327
327
  patterns: List[TestcasePattern],
328
328
  ) -> List[GenerationTestcaseEntry]:
329
329
  res: List[GenerationTestcaseEntry] = []
@@ -337,12 +337,12 @@ def extract_generation_testcases_from_patterns(
337
337
  pattern.intersecting_group(subgroup_path) for pattern in patterns
338
338
  )
339
339
 
340
- def visit(self, entry: GenerationTestcaseEntry):
340
+ async def visit(self, entry: GenerationTestcaseEntry):
341
341
  if not any(
342
342
  pattern.match(entry.group_entry) for pattern in patterns
343
343
  ) and not any(pattern.match(entry.subgroup_entry) for pattern in patterns):
344
344
  return
345
345
  res.append(entry)
346
346
 
347
- run_testcase_visitor(ExtractGenerationTestcasesVisitor())
347
+ await run_testcase_visitor(ExtractGenerationTestcasesVisitor())
348
348
  return res
rbx/box/testcases/main.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import pathlib
2
2
  from typing import Annotated, List, Optional
3
3
 
4
+ import syncer
4
5
  import typer
5
6
 
6
7
  from rbx import annotations, config, utils
@@ -20,8 +21,8 @@ from rbx.console import console
20
21
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
21
22
 
22
23
 
23
- def _find_testcase(entry: TestcaseEntry) -> GenerationTestcaseEntry:
24
- extracted = extract_generation_testcases([entry])
24
+ async def _find_testcase(entry: TestcaseEntry) -> GenerationTestcaseEntry:
25
+ extracted = await extract_generation_testcases([entry])
25
26
  if not extracted:
26
27
  console.print(f'[error]Testcase [item]{entry}[/item] not found.[/error]')
27
28
  raise typer.Exit(1)
@@ -35,7 +36,7 @@ def _should_generate_output(entry: GenerationTestcaseEntry) -> bool:
35
36
  ) and package.get_main_solution() is not None
36
37
 
37
38
 
38
- def _generate_input_for_editing(
39
+ async def _generate_input_for_editing(
39
40
  entry: GenerationTestcaseEntry,
40
41
  output: bool = True,
41
42
  progress: Optional[utils.StatusProgress] = None,
@@ -43,7 +44,7 @@ def _generate_input_for_editing(
43
44
  if (
44
45
  output and _should_generate_output(entry)
45
46
  ) or entry.metadata.copied_from is None:
46
- generate_standalone(
47
+ await generate_standalone(
47
48
  entry.metadata,
48
49
  validate=False,
49
50
  group_entry=entry.group_entry,
@@ -54,7 +55,7 @@ def _generate_input_for_editing(
54
55
  return entry.metadata.copied_to.inputPath
55
56
 
56
57
 
57
- def _generate_output_for_editing(
58
+ async def _generate_output_for_editing(
58
59
  entry: GenerationTestcaseEntry,
59
60
  progress: Optional[utils.StatusProgress] = None,
60
61
  ) -> Optional[pathlib.Path]:
@@ -65,29 +66,32 @@ def _generate_output_for_editing(
65
66
  return entry.metadata.copied_from.outputPath
66
67
  if not _should_generate_output(entry):
67
68
  return None
68
- generate_outputs_for_testcases([entry.group_entry], progress=progress)
69
+ await generate_outputs_for_testcases([entry.group_entry], progress=progress)
69
70
  return entry.metadata.copied_to.outputPath
70
71
 
71
72
 
72
- def _generate_for_editing(
73
+ async def _generate_for_editing(
73
74
  entry: GenerationTestcaseEntry,
74
75
  input: bool,
75
76
  output: bool,
76
77
  progress: Optional[utils.StatusProgress] = None,
77
78
  ) -> List[pathlib.Path]:
78
79
  res = []
79
- input_path = _generate_input_for_editing(entry, output=output, progress=progress)
80
+ input_path = await _generate_input_for_editing(
81
+ entry, output=output, progress=progress
82
+ )
80
83
  if input:
81
84
  res.append(input_path)
82
85
  if output:
83
- output_path = _generate_output_for_editing(entry, progress=progress)
86
+ output_path = await _generate_output_for_editing(entry, progress=progress)
84
87
  if output_path is not None:
85
88
  res.append(output_path)
86
89
  return res
87
90
 
88
91
 
89
92
  @app.command('view, v', help='View a testcase in your default editor.')
90
- def view(
93
+ @syncer.sync
94
+ async def view(
91
95
  tc: Annotated[
92
96
  str,
93
97
  typer.Argument(help='Testcase to view. Format: [group]/[index].'),
@@ -112,17 +116,18 @@ def view(
112
116
  raise typer.Exit(1)
113
117
 
114
118
  entry = TestcaseEntry.parse(tc)
115
- testcase = _find_testcase(entry)
119
+ testcase = await _find_testcase(entry)
116
120
 
117
121
  with utils.StatusProgress('Preparing testcase...') as s:
118
- items = _generate_for_editing(
122
+ items = await _generate_for_editing(
119
123
  testcase, input=not output_only, output=not input_only, progress=s
120
124
  )
121
125
  config.edit_multiple(items, readonly=True)
122
126
 
123
127
 
124
128
  @app.command('info, i', help='Show information about testcases.')
125
- def info(
129
+ @syncer.sync
130
+ async def info(
126
131
  pattern: Annotated[
127
132
  Optional[str],
128
133
  typer.Argument(
@@ -131,7 +136,7 @@ def info(
131
136
  ] = None,
132
137
  ):
133
138
  tc_pattern = TestcasePattern.parse(pattern or '*')
134
- testcases = extract_generation_testcases_from_patterns([tc_pattern])
139
+ testcases = await extract_generation_testcases_from_patterns([tc_pattern])
135
140
  if not testcases:
136
141
  console.print(
137
142
  f'[error]No testcases found matching pattern [item]{pattern}[/item].[/error]'