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/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 'lnumber'
109
+ return 'blue'
104
110
  if self.match(Outcome.MEMORY_LIMIT_EXCEEDED):
105
- return 'cyan'
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 = NameField(description='The name of the generator to call.')
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 check_first_solution_is_main(self):
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 generate_output_for_testcase, generate_standalone
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
- actual_sandbox = package.get_singleton_sandbox()
179
+ def run_fn(retry_index: int) -> Evaluation:
180
+ actual_sandbox = package.get_singleton_sandbox()
173
181
 
174
- limits = get_limits_for_language(
175
- solution.language, verification, timelimit_override
176
- )
182
+ limits = get_limits_for_language(
183
+ solution.language, verification, timelimit_override
184
+ )
177
185
 
178
- sandbox = EnvironmentSandbox()
179
- sandbox.timeLimit = limits.time
180
- if limits.isDoubleTL and sandbox.timeLimit is not None:
181
- # Double TL.
182
- sandbox.timeLimit = sandbox.timeLimit * 2
183
- sandbox.wallTimeLimit = sandbox.timeLimit
184
- if sandbox.timeLimit is not None and actual_sandbox.use_soft_timeout():
185
- sandbox.wallTimeLimit = sandbox.timeLimit * 2
186
- sandbox.memoryLimit = limits.memory
187
- sandbox.fileSizeLimit = limits.output
188
- extra_config = ExecutionConfig(sandbox=sandbox)
189
-
190
- output_path = output_dir / testcase.inputPath.with_suffix('.out').name
191
- error_path = output_path.with_suffix('.err')
192
- log_path = output_path.with_suffix('.log')
193
- output_path.parent.mkdir(parents=True, exist_ok=True)
194
-
195
- run_log = run_item(
196
- solution,
197
- DigestOrSource.create(compiled_digest),
198
- stdin=DigestOrSource.create(testcase.inputPath),
199
- stdout=DigestOrDest.create(output_path),
200
- stderr=DigestOrDest.create(error_path),
201
- extra_config=extra_config,
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
- if checker_digest is not None:
205
- checker_result = checkers.check(
206
- checker_digest,
207
- run_log,
208
- testcase,
209
- program_output=output_path,
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
- eval = Evaluation(
215
- result=checker_result,
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
- log_path.write_text(model_to_yaml(eval))
228
- return eval
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 _run_interactive_solutions(
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
- ) -> Iterator[EvaluationItem]:
423
- pkg = package.find_problem_package_or_die()
434
+ print: bool = False,
435
+ ) -> Testcase:
424
436
  main_solution = package.get_main_solution()
425
- check = check and main_solution is not None
426
-
427
- checker_digest = checkers.compile_checker() if check else None
428
- compiled_solutions = compile_solutions(
429
- progress=progress, tracked_solutions=tracked_solutions, sanitized=sanitized
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 main_solution is not None:
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 generator is not None:
462
- expanded_call = generate_standalone(generator, input_path)
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
- _print_solution_header(sol, console.console, is_irun=True)
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().replace('lnumber', 'cyan')
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 'lnumber'
693
+ return 'blue'
586
694
  if outcome == Outcome.MEMORY_LIMIT_EXCEEDED:
587
- return 'cyan'
695
+ return 'yellow'
588
696
  return 'magenta'
589
697
 
590
698