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.
- rbx/box/builder.py +6 -6
- rbx/box/checkers.py +100 -25
- rbx/box/cli.py +868 -0
- rbx/box/code.py +272 -84
- rbx/box/contest/statements.py +4 -2
- rbx/box/generators.py +55 -49
- rbx/box/generators_test.py +7 -7
- rbx/box/main.py +1 -868
- rbx/box/package.py +57 -2
- rbx/box/packaging/boca/packager.py +2 -1
- rbx/box/packaging/main.py +17 -9
- rbx/box/packaging/moj/packager.py +49 -10
- rbx/box/retries.py +5 -5
- rbx/box/schema.py +20 -4
- rbx/box/solutions.py +46 -108
- rbx/box/solutions_test.py +5 -6
- rbx/box/state.py +1 -0
- rbx/box/statements/build_statements.py +4 -2
- rbx/box/stresses.py +23 -12
- rbx/box/tasks.py +277 -0
- rbx/box/testcase_extractors.py +21 -21
- rbx/box/testcases/main.py +19 -14
- rbx/box/unit.py +10 -7
- rbx/box/validators.py +10 -10
- rbx/box/validators_test.py +3 -3
- rbx/grading/judge/cacher.py +0 -4
- rbx/grading/judge/digester.py +0 -3
- rbx/grading/judge/sandbox.py +15 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +20 -6
- rbx/grading/judge/sandboxes/timeit.py +117 -7
- rbx/grading/judge/storage.py +0 -4
- rbx/grading/steps.py +76 -2
- rbx/grading/steps_with_caching.py +45 -3
- rbx/grading/steps_with_caching_run_test.py +51 -49
- rbx/main.py +0 -4
- rbx/resources/packagers/moj/scripts/compare.sh +25 -6
- rbx/test.py +6 -4
- {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/METADATA +2 -2
- {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/RECORD +42 -55
- {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/WHEEL +1 -1
- rbx/testdata/box1/gen1.cpp +0 -7
- rbx/testdata/box1/gen2.cpp +0 -9
- rbx/testdata/box1/genScript.py +0 -2
- rbx/testdata/box1/hard-tle.sol.cpp +0 -26
- rbx/testdata/box1/ole.cpp +0 -17
- rbx/testdata/box1/problem.rbx.yml +0 -39
- rbx/testdata/box1/re.sol.cpp +0 -23
- rbx/testdata/box1/sol.cpp +0 -22
- rbx/testdata/box1/tests/1.in +0 -1
- rbx/testdata/box1/tle-and-incorrect.sol.cpp +0 -33
- rbx/testdata/box1/tle.sol.cpp +0 -35
- rbx/testdata/box1/validator.cpp +0 -11
- rbx/testdata/box1/wa.sol.cpp +0 -22
- rbx/testdata/caching/executable.py +0 -1
- rbx/testdata/compatible +0 -0
- {rbx_cp-0.5.40.dist-info → rbx_cp-0.5.45.dist-info}/LICENSE +0 -0
- {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
|
-
|
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
|
-
|
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(
|
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)
|
rbx/box/testcase_extractors.py
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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]'
|