rbx.cp 0.5.27__py3-none-any.whl → 0.5.29__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 +5 -5
- rbx/box/code.py +2 -0
- rbx/box/generators.py +403 -276
- rbx/box/main.py +10 -7
- rbx/box/retries.py +143 -0
- rbx/box/schema.py +15 -6
- rbx/box/setter_config.py +22 -0
- rbx/box/solutions.py +209 -101
- rbx/box/stresses.py +62 -33
- rbx/box/stressing/finder_parser.py +2 -2
- rbx/box/testcases.py +17 -1
- rbx/grading/steps.py +1 -0
- rbx/grading/steps_with_caching.py +5 -3
- rbx/resources/default_setter_config.mac.yml +8 -0
- rbx/resources/default_setter_config.yml +8 -0
- rbx/utils.py +8 -2
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/METADATA +1 -4
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/RECORD +21 -20
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.27.dist-info → rbx_cp-0.5.29.dist-info}/entry_points.txt +0 -0
rbx/box/main.py
CHANGED
@@ -48,6 +48,7 @@ from rbx.box.solutions import (
|
|
48
48
|
run_solutions,
|
49
49
|
)
|
50
50
|
from rbx.box.statements import build_statements
|
51
|
+
from rbx.box.testcases import TestcaseEntry
|
51
52
|
|
52
53
|
app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
|
53
54
|
app.add_typer(
|
@@ -343,6 +344,14 @@ def irun(
|
|
343
344
|
'-g',
|
344
345
|
help='Generator call to use to generate a single test for execution.',
|
345
346
|
),
|
347
|
+
testcase: Optional[str] = typer.Option(
|
348
|
+
None,
|
349
|
+
'--testcase',
|
350
|
+
'--test',
|
351
|
+
'-tc',
|
352
|
+
'-t',
|
353
|
+
help='Testcase to run, in the format "[group]/[index]". If not specified, will run interactively.',
|
354
|
+
),
|
346
355
|
print: bool = typer.Option(
|
347
356
|
False, '--print', '-p', help='Whether to print outputs to terminal.'
|
348
357
|
),
|
@@ -370,13 +379,6 @@ def irun(
|
|
370
379
|
)
|
371
380
|
return
|
372
381
|
|
373
|
-
main_solution = package.get_main_solution()
|
374
|
-
if check and main_solution is None:
|
375
|
-
console.console.print(
|
376
|
-
'[warning]No main solution found, running without checkers.[/warning]'
|
377
|
-
)
|
378
|
-
check = False
|
379
|
-
|
380
382
|
tracked_solutions = None
|
381
383
|
if outcome is not None:
|
382
384
|
tracked_solutions = {
|
@@ -411,6 +413,7 @@ def irun(
|
|
411
413
|
generator=generators.get_call_from_string(generator)
|
412
414
|
if generator is not None
|
413
415
|
else None,
|
416
|
+
testcase_entry=TestcaseEntry.parse(testcase) if testcase else None,
|
414
417
|
print=print,
|
415
418
|
sanitized=sanitized,
|
416
419
|
)
|
rbx/box/retries.py
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import pathlib
|
3
|
+
import shutil
|
4
|
+
import tempfile
|
5
|
+
from contextlib import contextmanager
|
6
|
+
from typing import Callable, List, Optional
|
7
|
+
|
8
|
+
from rbx.box import package
|
9
|
+
from rbx.box.setter_config import RepeatsConfig, get_setter_config
|
10
|
+
from rbx.grading.steps import Evaluation, Outcome
|
11
|
+
|
12
|
+
|
13
|
+
def _both_accepted(eval_a: Evaluation, eval_b: Evaluation) -> bool:
|
14
|
+
return (
|
15
|
+
eval_a.result.outcome == Outcome.ACCEPTED
|
16
|
+
and eval_b.result.outcome == Outcome.ACCEPTED
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
def _any_tle(eval_a: Evaluation, eval_b: Evaluation) -> bool:
|
21
|
+
return (
|
22
|
+
eval_a.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
|
23
|
+
or eval_b.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
def _get_faster(eval_a: Evaluation, eval_b: Evaluation) -> Evaluation:
|
28
|
+
if eval_a.log.time is None:
|
29
|
+
return eval_b
|
30
|
+
if eval_b.log.time is None:
|
31
|
+
return eval_a
|
32
|
+
if eval_a.log.time < eval_b.log.time:
|
33
|
+
return eval_a
|
34
|
+
return eval_b
|
35
|
+
|
36
|
+
|
37
|
+
def _merge_evaluations(eval_a: Evaluation, eval_b: Evaluation) -> Evaluation:
|
38
|
+
if _both_accepted(eval_a, eval_b) or _any_tle(eval_a, eval_b):
|
39
|
+
return _get_faster(eval_a, eval_b)
|
40
|
+
if eval_a.result.outcome != Outcome.ACCEPTED:
|
41
|
+
return eval_a
|
42
|
+
if eval_b.result.outcome != Outcome.ACCEPTED:
|
43
|
+
return eval_b
|
44
|
+
return _get_faster(eval_a, eval_b)
|
45
|
+
|
46
|
+
|
47
|
+
@contextmanager
|
48
|
+
def _temp_retry_dir():
|
49
|
+
"""Create a temporary directory for retry artifacts."""
|
50
|
+
temp_dir = tempfile.mkdtemp(prefix='rbx_retry_')
|
51
|
+
try:
|
52
|
+
yield pathlib.Path(temp_dir)
|
53
|
+
finally:
|
54
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
55
|
+
|
56
|
+
|
57
|
+
@dataclasses.dataclass
|
58
|
+
class FileToRecover:
|
59
|
+
from_path: pathlib.Path
|
60
|
+
to_path: pathlib.Path
|
61
|
+
|
62
|
+
|
63
|
+
def _move_to_temp_dir(path: pathlib.Path, temp_dir: pathlib.Path) -> FileToRecover:
|
64
|
+
problem_path = package.find_problem()
|
65
|
+
path = path.resolve()
|
66
|
+
temp_dir = temp_dir.resolve()
|
67
|
+
relative = path.relative_to(problem_path)
|
68
|
+
|
69
|
+
temp_path = temp_dir / relative
|
70
|
+
temp_path.parent.mkdir(parents=True, exist_ok=True)
|
71
|
+
shutil.move(path, temp_path)
|
72
|
+
return FileToRecover(temp_path, path)
|
73
|
+
|
74
|
+
|
75
|
+
def _move_logs_to_temp_dir(
|
76
|
+
eval: Evaluation, temp_dir: pathlib.Path
|
77
|
+
) -> List[FileToRecover]:
|
78
|
+
recover = []
|
79
|
+
if (
|
80
|
+
eval.log.stdout_absolute_path is not None
|
81
|
+
and eval.log.stdout_absolute_path.exists()
|
82
|
+
):
|
83
|
+
recover.append(_move_to_temp_dir(eval.log.stdout_absolute_path, temp_dir))
|
84
|
+
if (
|
85
|
+
eval.log.stderr_absolute_path is not None
|
86
|
+
and eval.log.stderr_absolute_path.exists()
|
87
|
+
):
|
88
|
+
recover.append(_move_to_temp_dir(eval.log.stderr_absolute_path, temp_dir))
|
89
|
+
if eval.log.log_absolute_path is not None and eval.log.log_absolute_path.exists():
|
90
|
+
recover.append(_move_to_temp_dir(eval.log.log_absolute_path, temp_dir))
|
91
|
+
return recover
|
92
|
+
|
93
|
+
|
94
|
+
class Retrier:
|
95
|
+
def __init__(self, config: Optional[RepeatsConfig] = None, is_stress: bool = False):
|
96
|
+
self.config = config or get_setter_config().repeats
|
97
|
+
self.is_stress = is_stress
|
98
|
+
|
99
|
+
self.reset()
|
100
|
+
|
101
|
+
def reset(self):
|
102
|
+
self.reps = self.config.reps - 1
|
103
|
+
self.retries = self.config.retries
|
104
|
+
self.retries_for_stress = self.config.retries_for_stress
|
105
|
+
self.retry_index = 0
|
106
|
+
|
107
|
+
def repeat(
|
108
|
+
self,
|
109
|
+
func: Callable[[int], Evaluation],
|
110
|
+
) -> Evaluation:
|
111
|
+
self.retry_index += 1
|
112
|
+
eval = func(self.retry_index)
|
113
|
+
if self.should_repeat(eval):
|
114
|
+
with _temp_retry_dir() as temp_dir:
|
115
|
+
# Move files to temp dir to open run for repeat.
|
116
|
+
recover = _move_logs_to_temp_dir(eval, temp_dir)
|
117
|
+
# Actually repeat and choose the best evaluation.
|
118
|
+
next_eval = self.repeat(func)
|
119
|
+
chosen_eval = _merge_evaluations(eval, next_eval)
|
120
|
+
|
121
|
+
if id(chosen_eval) == id(eval):
|
122
|
+
# Must recover originally moved files.
|
123
|
+
for file in recover:
|
124
|
+
file.to_path.parent.mkdir(parents=True, exist_ok=True)
|
125
|
+
shutil.move(file.from_path, file.to_path)
|
126
|
+
return chosen_eval
|
127
|
+
return eval
|
128
|
+
|
129
|
+
def should_repeat(self, eval: Evaluation) -> bool:
|
130
|
+
if self.is_stress:
|
131
|
+
if (
|
132
|
+
eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED
|
133
|
+
and self.retries_for_stress > 0
|
134
|
+
):
|
135
|
+
self.retries_for_stress -= 1
|
136
|
+
return True
|
137
|
+
if eval.result.outcome == Outcome.TIME_LIMIT_EXCEEDED and self.retries > 0:
|
138
|
+
self.retries -= 1
|
139
|
+
return True
|
140
|
+
if self.reps > 0:
|
141
|
+
self.reps -= 1
|
142
|
+
return True
|
143
|
+
return False
|
rbx/box/schema.py
CHANGED
@@ -20,6 +20,12 @@ def NameField(**kwargs):
|
|
20
20
|
)
|
21
21
|
|
22
22
|
|
23
|
+
def FNameField(**kwargs):
|
24
|
+
return Field(
|
25
|
+
pattern=r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*$', min_length=3, max_length=128, **kwargs
|
26
|
+
)
|
27
|
+
|
28
|
+
|
23
29
|
def _check_oneof(model_obj: BaseModel, fields: List[str]):
|
24
30
|
has = []
|
25
31
|
for field in fields:
|
@@ -100,9 +106,9 @@ class ExpectedOutcome(AutoEnum):
|
|
100
106
|
if self.match(Outcome.TIME_LIMIT_EXCEEDED):
|
101
107
|
return 'yellow'
|
102
108
|
if self.match(Outcome.RUNTIME_ERROR):
|
103
|
-
return '
|
109
|
+
return 'blue'
|
104
110
|
if self.match(Outcome.MEMORY_LIMIT_EXCEEDED):
|
105
|
-
return '
|
111
|
+
return 'yellow'
|
106
112
|
return 'magenta'
|
107
113
|
|
108
114
|
def is_slow(self) -> bool:
|
@@ -173,14 +179,14 @@ class Testcase(BaseModel):
|
|
173
179
|
inputPath: pathlib.Path = Field(description="""The path of the input file.""")
|
174
180
|
|
175
181
|
outputPath: Optional[pathlib.Path] = Field(
|
176
|
-
None, description="""The path of the output file."""
|
182
|
+
default=None, description="""The path of the output file."""
|
177
183
|
)
|
178
184
|
|
179
185
|
|
180
186
|
class GeneratorCall(BaseModel):
|
181
187
|
model_config = ConfigDict(extra='forbid')
|
182
188
|
|
183
|
-
name: str =
|
189
|
+
name: str = FNameField(description='The name of the generator to call.')
|
184
190
|
|
185
191
|
args: Optional[str] = Field(
|
186
192
|
None, description='The arguments to pass to the generator.'
|
@@ -403,12 +409,15 @@ that is correct and used as reference -- and should have the `accepted` outcome.
|
|
403
409
|
return res
|
404
410
|
|
405
411
|
@model_validator(mode='after')
|
406
|
-
def
|
412
|
+
def check_first_solution_is_main_if_there_is_ac(self):
|
413
|
+
if all(sol.outcome != Outcome.ACCEPTED for sol in self.solutions):
|
414
|
+
# No main solution.
|
415
|
+
return self
|
407
416
|
if self.solutions:
|
408
417
|
if self.solutions[0].outcome != ExpectedOutcome.ACCEPTED:
|
409
418
|
raise PydanticCustomError(
|
410
419
|
'MISSING_MAIN_SOLUTION',
|
411
|
-
'The first solution in the package must have the "ACCEPTED" outcome.',
|
420
|
+
'The first solution in the package must have the "ACCEPTED" outcome if there are ACCEPTED solutions.',
|
412
421
|
)
|
413
422
|
return self
|
414
423
|
|
rbx/box/setter_config.py
CHANGED
@@ -36,6 +36,23 @@ class WarningsConfig(BaseModel):
|
|
36
36
|
)
|
37
37
|
|
38
38
|
|
39
|
+
class RepeatsConfig(BaseModel):
|
40
|
+
reps: int = Field(
|
41
|
+
1,
|
42
|
+
description='Number of times to repeat the solution.',
|
43
|
+
)
|
44
|
+
|
45
|
+
retries: int = Field(
|
46
|
+
0,
|
47
|
+
description='Number of times to retry if the solution TLs.',
|
48
|
+
)
|
49
|
+
|
50
|
+
retries_for_stress: int = Field(
|
51
|
+
0,
|
52
|
+
description='Number of times to retry in stress mode if the solution TLs.',
|
53
|
+
)
|
54
|
+
|
55
|
+
|
39
56
|
class SetterConfig(BaseModel):
|
40
57
|
sanitizers: SanitizersConfig = Field(
|
41
58
|
default_factory=SanitizersConfig, # type: ignore
|
@@ -46,6 +63,11 @@ class SetterConfig(BaseModel):
|
|
46
63
|
description='Configuration for warnings.',
|
47
64
|
)
|
48
65
|
|
66
|
+
repeats: RepeatsConfig = Field(
|
67
|
+
default_factory=RepeatsConfig, # type: ignore
|
68
|
+
description='Configuration for repeats.',
|
69
|
+
)
|
70
|
+
|
49
71
|
command_substitutions: Dict[str, str] = Field(
|
50
72
|
{},
|
51
73
|
description='Substitutions to apply to commands before running them.',
|
rbx/box/solutions.py
CHANGED
@@ -16,7 +16,7 @@ import rich.text
|
|
16
16
|
import typer
|
17
17
|
from pydantic import BaseModel
|
18
18
|
|
19
|
-
from rbx import console
|
19
|
+
from rbx import console, utils
|
20
20
|
from rbx.box import checkers, package
|
21
21
|
from rbx.box.code import SanitizationLevel, compile_item, find_language_name, run_item
|
22
22
|
from rbx.box.deferred import Deferred
|
@@ -25,7 +25,14 @@ from rbx.box.environment import (
|
|
25
25
|
ExecutionConfig,
|
26
26
|
VerificationLevel,
|
27
27
|
)
|
28
|
-
from rbx.box.generators import
|
28
|
+
from rbx.box.generators import (
|
29
|
+
GenerationMetadata,
|
30
|
+
expand_generator_call,
|
31
|
+
extract_generation_testcases,
|
32
|
+
generate_output_for_testcase,
|
33
|
+
generate_standalone,
|
34
|
+
)
|
35
|
+
from rbx.box.retries import Retrier
|
29
36
|
from rbx.box.schema import (
|
30
37
|
ExpectedOutcome,
|
31
38
|
GeneratorCall,
|
@@ -34,7 +41,7 @@ from rbx.box.schema import (
|
|
34
41
|
Testcase,
|
35
42
|
TestcaseGroup,
|
36
43
|
)
|
37
|
-
from rbx.box.testcases import find_built_testcases
|
44
|
+
from rbx.box.testcases import TestcaseEntry, find_built_testcases
|
38
45
|
from rbx.grading.steps import (
|
39
46
|
DigestOrDest,
|
40
47
|
DigestOrSource,
|
@@ -169,63 +176,70 @@ def _run_solution_on_testcase(
|
|
169
176
|
verification: VerificationLevel = VerificationLevel.NONE,
|
170
177
|
timelimit_override: Optional[int] = None,
|
171
178
|
) -> Evaluation:
|
172
|
-
|
179
|
+
def run_fn(retry_index: int) -> Evaluation:
|
180
|
+
actual_sandbox = package.get_singleton_sandbox()
|
173
181
|
|
174
|
-
|
175
|
-
|
176
|
-
|
182
|
+
limits = get_limits_for_language(
|
183
|
+
solution.language, verification, timelimit_override
|
184
|
+
)
|
177
185
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
186
|
+
sandbox = EnvironmentSandbox()
|
187
|
+
sandbox.timeLimit = limits.time
|
188
|
+
if limits.isDoubleTL and sandbox.timeLimit is not None:
|
189
|
+
# Double TL.
|
190
|
+
sandbox.timeLimit = sandbox.timeLimit * 2
|
191
|
+
sandbox.wallTimeLimit = sandbox.timeLimit
|
192
|
+
if sandbox.timeLimit is not None and actual_sandbox.use_soft_timeout():
|
193
|
+
sandbox.wallTimeLimit = sandbox.timeLimit * 2
|
194
|
+
sandbox.memoryLimit = limits.memory
|
195
|
+
sandbox.fileSizeLimit = limits.output
|
196
|
+
extra_config = ExecutionConfig(sandbox=sandbox)
|
197
|
+
|
198
|
+
output_path = output_dir / testcase.inputPath.with_suffix('.out').name
|
199
|
+
error_path = output_path.with_suffix('.err')
|
200
|
+
log_path = output_path.with_suffix('.log')
|
201
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
202
|
+
|
203
|
+
run_log = run_item(
|
204
|
+
solution,
|
205
|
+
DigestOrSource.create(compiled_digest),
|
206
|
+
stdin=DigestOrSource.create(testcase.inputPath),
|
207
|
+
stdout=DigestOrDest.create(output_path),
|
208
|
+
stderr=DigestOrDest.create(error_path),
|
209
|
+
extra_config=extra_config,
|
210
|
+
retry_index=retry_index,
|
211
|
+
)
|
203
212
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
213
|
+
if checker_digest is not None:
|
214
|
+
checker_result = checkers.check(
|
215
|
+
checker_digest,
|
216
|
+
run_log,
|
217
|
+
testcase,
|
218
|
+
program_output=output_path,
|
219
|
+
)
|
220
|
+
else:
|
221
|
+
checker_result = checkers.check_with_no_output(run_log)
|
222
|
+
|
223
|
+
eval = Evaluation(
|
224
|
+
result=checker_result,
|
225
|
+
testcase=TestcaseIO(
|
226
|
+
index=testcase_index,
|
227
|
+
input=testcase.inputPath,
|
228
|
+
output=testcase.outputPath,
|
229
|
+
),
|
230
|
+
log=TestcaseLog(
|
231
|
+
**(run_log.model_dump() if run_log is not None else {}),
|
232
|
+
stdout_absolute_path=output_path.absolute(),
|
233
|
+
stderr_absolute_path=error_path.absolute(),
|
234
|
+
log_absolute_path=log_path.absolute(),
|
235
|
+
),
|
210
236
|
)
|
211
|
-
else:
|
212
|
-
checker_result = checkers.check_with_no_output(run_log)
|
213
237
|
|
214
|
-
|
215
|
-
|
216
|
-
testcase=TestcaseIO(
|
217
|
-
index=testcase_index, input=testcase.inputPath, output=testcase.outputPath
|
218
|
-
),
|
219
|
-
log=TestcaseLog(
|
220
|
-
**(run_log.model_dump() if run_log is not None else {}),
|
221
|
-
stdout_absolute_path=output_path.absolute(),
|
222
|
-
stderr_absolute_path=error_path.absolute(),
|
223
|
-
log_absolute_path=log_path.absolute(),
|
224
|
-
),
|
225
|
-
)
|
238
|
+
log_path.write_text(model_to_yaml(eval))
|
239
|
+
return eval
|
226
240
|
|
227
|
-
|
228
|
-
return
|
241
|
+
retrier = Retrier()
|
242
|
+
return retrier.repeat(run_fn)
|
229
243
|
|
230
244
|
|
231
245
|
def _run_solution(
|
@@ -411,26 +425,97 @@ def run_solutions(
|
|
411
425
|
)
|
412
426
|
|
413
427
|
|
414
|
-
def
|
428
|
+
async def _generate_testcase_interactively(
|
415
429
|
progress: Optional[StatusProgress] = None,
|
416
|
-
tracked_solutions: Optional[Set[str]] = None,
|
417
|
-
verification: VerificationLevel = VerificationLevel.NONE,
|
418
430
|
generator: Optional[GeneratorCall] = None,
|
431
|
+
testcase_entry: Optional[TestcaseEntry] = None,
|
419
432
|
check: bool = True,
|
420
|
-
print: bool = False,
|
421
433
|
sanitized: bool = False,
|
422
|
-
|
423
|
-
|
434
|
+
print: bool = False,
|
435
|
+
) -> Testcase:
|
424
436
|
main_solution = package.get_main_solution()
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
437
|
+
irun_dir = package.get_problem_iruns_dir()
|
438
|
+
inputs_dir = irun_dir / 'inputs'
|
439
|
+
inputs_dir.mkdir(parents=True, exist_ok=True)
|
440
|
+
testcase = Testcase(
|
441
|
+
inputPath=inputs_dir / '000.in',
|
442
|
+
outputPath=(inputs_dir / '000.out') if check else None,
|
430
443
|
)
|
431
444
|
|
445
|
+
is_manual = False
|
446
|
+
generation_metadata = None
|
447
|
+
if generator is not None:
|
448
|
+
generation_metadata = GenerationMetadata(
|
449
|
+
generator_call=expand_generator_call(generator),
|
450
|
+
copied_to=testcase,
|
451
|
+
)
|
452
|
+
elif testcase_entry is not None:
|
453
|
+
extracted = extract_generation_testcases([testcase_entry])
|
454
|
+
if not extracted:
|
455
|
+
console.console.print(
|
456
|
+
f'[error]Failed searching for testcase [item]{testcase_entry}[/item].[/error]'
|
457
|
+
)
|
458
|
+
raise typer.Exit(1)
|
459
|
+
generation_metadata = extracted[0].metadata
|
460
|
+
# Replace destination with the irun testcase we're using.
|
461
|
+
generation_metadata.copied_to = testcase
|
462
|
+
else:
|
463
|
+
with utils.no_progress(progress):
|
464
|
+
input = console.multiline_prompt('Testcase input')
|
465
|
+
testcase.inputPath.write_text(input)
|
466
|
+
console.console.print()
|
467
|
+
|
468
|
+
if (
|
469
|
+
testcase.outputPath is not None
|
470
|
+
and not testcase.outputPath.is_file()
|
471
|
+
and main_solution is None
|
472
|
+
):
|
473
|
+
with utils.no_progress(progress):
|
474
|
+
output = console.multiline_prompt('Testcase output')
|
475
|
+
testcase.outputPath.write_text(output)
|
476
|
+
console.console.print()
|
477
|
+
|
478
|
+
generation_metadata = GenerationMetadata(
|
479
|
+
copied_to=testcase,
|
480
|
+
)
|
481
|
+
is_manual = True
|
482
|
+
|
483
|
+
# 1. Generate testcase.
|
484
|
+
if generation_metadata is not None:
|
485
|
+
generate_standalone(
|
486
|
+
generation_metadata,
|
487
|
+
progress=progress,
|
488
|
+
validate=True,
|
489
|
+
)
|
490
|
+
if testcase_entry is not None:
|
491
|
+
console.console.print(
|
492
|
+
f'Using input from testcase [item]{testcase_entry}[/item].'
|
493
|
+
)
|
494
|
+
elif generation_metadata.generator_call is not None:
|
495
|
+
console.console.print(
|
496
|
+
f'Using input from generator call [item]{generation_metadata.generator_call.name} {generation_metadata.generator_call.args}[/item].'
|
497
|
+
)
|
498
|
+
if print and not is_manual:
|
499
|
+
console.console.print(testcase.inputPath.read_text())
|
500
|
+
else:
|
501
|
+
console.console.print(
|
502
|
+
f'Input was written to [item]{testcase.inputPath.resolve()}[/item]'
|
503
|
+
)
|
504
|
+
console.console.print()
|
505
|
+
|
506
|
+
# 2. Generate test output from reference
|
432
507
|
main_solution_digest = None
|
433
|
-
if check and
|
508
|
+
if check and not (
|
509
|
+
testcase.outputPath is not None and testcase.outputPath.is_file()
|
510
|
+
):
|
511
|
+
if main_solution is None:
|
512
|
+
console.console.print(
|
513
|
+
'[error]Checking is enabled but no main solution or custom output was specified.[/error]'
|
514
|
+
)
|
515
|
+
raise typer.Exit(1)
|
516
|
+
|
517
|
+
if progress:
|
518
|
+
progress.update('Compiling main solution...')
|
434
519
|
try:
|
435
520
|
main_solution_digest = compile_item(
|
436
521
|
main_solution,
|
@@ -444,6 +529,42 @@ def _run_interactive_solutions(
|
|
444
529
|
)
|
445
530
|
raise
|
446
531
|
|
532
|
+
if main_solution_digest is not None:
|
533
|
+
if progress:
|
534
|
+
progress.update('Generating output for test...')
|
535
|
+
# TODO: Add stderr path
|
536
|
+
generate_output_for_testcase(main_solution_digest, testcase)
|
537
|
+
|
538
|
+
if check and testcase.outputPath is not None and not testcase.outputPath.is_file():
|
539
|
+
# Output was not created, throw an error.
|
540
|
+
console.console.print(
|
541
|
+
'[error]Checking is enabled but no output could be generated for this testcase.[/error]'
|
542
|
+
)
|
543
|
+
console.console.print(
|
544
|
+
'[error]Either specify it explicitly or provide a main solution.[/error]'
|
545
|
+
)
|
546
|
+
raise typer.Exit(1)
|
547
|
+
|
548
|
+
return testcase
|
549
|
+
|
550
|
+
|
551
|
+
def _run_interactive_solutions(
|
552
|
+
testcase: Testcase,
|
553
|
+
progress: Optional[StatusProgress] = None,
|
554
|
+
tracked_solutions: Optional[Set[str]] = None,
|
555
|
+
verification: VerificationLevel = VerificationLevel.NONE,
|
556
|
+
check: bool = True,
|
557
|
+
sanitized: bool = False,
|
558
|
+
) -> Iterator[EvaluationItem]:
|
559
|
+
pkg = package.find_problem_package_or_die()
|
560
|
+
|
561
|
+
if check and progress:
|
562
|
+
progress.update('Compiling checker...')
|
563
|
+
checker_digest = checkers.compile_checker() if check else None
|
564
|
+
compiled_solutions = compile_solutions(
|
565
|
+
progress=progress, tracked_solutions=tracked_solutions, sanitized=sanitized
|
566
|
+
)
|
567
|
+
|
447
568
|
solutions = list(enumerate(pkg.solutions))
|
448
569
|
if tracked_solutions is not None:
|
449
570
|
solutions = [
|
@@ -451,33 +572,9 @@ def _run_interactive_solutions(
|
|
451
572
|
]
|
452
573
|
|
453
574
|
irun_dir = package.get_problem_iruns_dir()
|
454
|
-
shutil.rmtree(str(irun_dir), ignore_errors=True)
|
455
|
-
irun_dir.mkdir(parents=True, exist_ok=True)
|
456
|
-
inputs_dir = irun_dir / 'inputs'
|
457
|
-
inputs_dir.mkdir(parents=True, exist_ok=True)
|
458
|
-
input_path = inputs_dir / '000.in'
|
459
|
-
output_path = input_path.with_suffix('.out')
|
460
575
|
|
461
|
-
if
|
462
|
-
|
463
|
-
console.console.print(
|
464
|
-
f'Using input from generator call [item]{expanded_call.name} {expanded_call.args}[/item].'
|
465
|
-
)
|
466
|
-
if print:
|
467
|
-
console.console.print(input_path.read_text())
|
468
|
-
else:
|
469
|
-
console.console.print(
|
470
|
-
f'Input was written to [item]{input_path.resolve()}[/item]'
|
471
|
-
)
|
472
|
-
console.console.print()
|
473
|
-
else:
|
474
|
-
input = console.multiline_prompt('Testcase input')
|
475
|
-
input_path.write_text(input)
|
476
|
-
testcase = Testcase(inputPath=input_path, outputPath=output_path if check else None)
|
477
|
-
|
478
|
-
if main_solution_digest is not None:
|
479
|
-
# TODO: Add stderr path
|
480
|
-
generate_output_for_testcase(main_solution_digest, testcase)
|
576
|
+
if progress:
|
577
|
+
progress.update('Running solutions...')
|
481
578
|
|
482
579
|
for i, solution in solutions:
|
483
580
|
output_dir = irun_dir / f'{i}'
|
@@ -505,27 +602,38 @@ async def run_and_print_interactive_solutions(
|
|
505
602
|
tracked_solutions: Optional[Set[str]] = None,
|
506
603
|
verification: VerificationLevel = VerificationLevel.NONE,
|
507
604
|
generator: Optional[GeneratorCall] = None,
|
605
|
+
testcase_entry: Optional[TestcaseEntry] = None,
|
508
606
|
check: bool = True,
|
509
607
|
print: bool = False,
|
510
608
|
sanitized: bool = False,
|
511
609
|
):
|
610
|
+
# Ensure path is new.
|
611
|
+
irun_dir = package.get_problem_iruns_dir()
|
612
|
+
shutil.rmtree(str(irun_dir), ignore_errors=True)
|
613
|
+
irun_dir.mkdir(parents=True, exist_ok=True)
|
614
|
+
|
512
615
|
pkg = package.find_problem_package_or_die()
|
616
|
+
testcase = await _generate_testcase_interactively(
|
617
|
+
progress=progress,
|
618
|
+
generator=generator,
|
619
|
+
testcase_entry=testcase_entry,
|
620
|
+
check=check,
|
621
|
+
sanitized=sanitized,
|
622
|
+
print=print,
|
623
|
+
)
|
513
624
|
items = _run_interactive_solutions(
|
625
|
+
testcase,
|
514
626
|
progress=progress,
|
515
627
|
tracked_solutions=tracked_solutions,
|
516
628
|
verification=verification,
|
517
629
|
check=check,
|
518
|
-
generator=generator,
|
519
630
|
sanitized=sanitized,
|
520
|
-
print=print,
|
521
631
|
)
|
522
632
|
|
523
|
-
if progress:
|
524
|
-
progress.stop()
|
525
|
-
|
526
633
|
for item in items:
|
527
634
|
sol = pkg.solutions[item.solution_index]
|
528
|
-
|
635
|
+
with utils.no_progress(progress):
|
636
|
+
_print_solution_header(sol, console.console, is_irun=True)
|
529
637
|
|
530
638
|
eval = await item.eval()
|
531
639
|
|
@@ -549,7 +657,7 @@ async def run_and_print_interactive_solutions(
|
|
549
657
|
|
550
658
|
|
551
659
|
def _get_solution_repr(sol: Solution) -> List[Tuple[str, str]]:
|
552
|
-
fg_color = sol.outcome.style()
|
660
|
+
fg_color = sol.outcome.style()
|
553
661
|
return [
|
554
662
|
('', f'{str(sol.path)} '),
|
555
663
|
(f'fg:{fg_color}', sol.outcome.name),
|
@@ -582,9 +690,9 @@ def get_outcome_style_verdict(outcome: Outcome) -> str:
|
|
582
690
|
if outcome == Outcome.TIME_LIMIT_EXCEEDED:
|
583
691
|
return 'yellow'
|
584
692
|
if outcome == Outcome.RUNTIME_ERROR:
|
585
|
-
return '
|
693
|
+
return 'blue'
|
586
694
|
if outcome == Outcome.MEMORY_LIMIT_EXCEEDED:
|
587
|
-
return '
|
695
|
+
return 'yellow'
|
588
696
|
return 'magenta'
|
589
697
|
|
590
698
|
|