rbx.cp 0.5.39__py3-none-any.whl → 0.5.42__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 (53) hide show
  1. rbx/box/builder.py +6 -6
  2. rbx/box/checkers.py +105 -26
  3. rbx/box/cli.py +860 -0
  4. rbx/box/code.py +199 -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 -852
  9. rbx/box/package.py +42 -1
  10. rbx/box/packaging/boca/packager.py +2 -1
  11. rbx/box/packaging/main.py +24 -7
  12. rbx/box/packaging/moj/packager.py +164 -0
  13. rbx/box/retries.py +5 -5
  14. rbx/box/schema.py +86 -4
  15. rbx/box/solutions.py +46 -108
  16. rbx/box/solutions_test.py +5 -6
  17. rbx/box/statements/build_statements.py +4 -2
  18. rbx/box/stresses.py +23 -12
  19. rbx/box/tasks.py +258 -0
  20. rbx/box/testcase_extractors.py +21 -21
  21. rbx/box/testcases/main.py +19 -14
  22. rbx/box/unit.py +116 -0
  23. rbx/box/validators.py +27 -18
  24. rbx/box/validators_test.py +3 -3
  25. rbx/grading/judge/sandbox.py +8 -0
  26. rbx/grading/judge/sandboxes/stupid_sandbox.py +12 -7
  27. rbx/grading/judge/sandboxes/timeit.py +8 -2
  28. rbx/grading/steps.py +76 -2
  29. rbx/grading/steps_with_caching.py +45 -3
  30. rbx/grading/steps_with_caching_run_test.py +51 -49
  31. rbx/resources/packagers/moj/scripts/compare.sh +101 -0
  32. rbx/test.py +6 -4
  33. rbx/testdata/interactive/checker.cpp +21 -0
  34. rbx/testdata/interactive/gen.cpp +11 -0
  35. rbx/testdata/interactive/interactor.cpp +63 -0
  36. rbx/testdata/interactive/problem.rbx.yml +40 -0
  37. rbx/testdata/interactive/sols/af_ac_pe.cpp +75 -0
  38. rbx/testdata/interactive/sols/af_ac_re.cpp +76 -0
  39. rbx/testdata/interactive/sols/af_ac_too_many_iter.cpp +72 -0
  40. rbx/testdata/interactive/sols/af_inf_cout_with_flush.cpp +79 -0
  41. rbx/testdata/interactive/sols/af_inf_cout_without_flush.cpp +78 -0
  42. rbx/testdata/interactive/sols/af_ml.cpp +78 -0
  43. rbx/testdata/interactive/sols/af_tl_after_ans.cpp +74 -0
  44. rbx/testdata/interactive/sols/af_wa.cpp +74 -0
  45. rbx/testdata/interactive/sols/interactive-binary-search_mm_naive_cin.cpp +17 -0
  46. rbx/testdata/interactive/sols/main.cpp +26 -0
  47. rbx/testdata/interactive/testplan.txt +6 -0
  48. rbx/testdata/interactive/validator.cpp +16 -0
  49. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/METADATA +2 -1
  50. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/RECORD +53 -32
  51. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/LICENSE +0 -0
  52. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/WHEEL +0 -0
  53. {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/entry_points.txt +0 -0
rbx/box/package.py CHANGED
@@ -1,5 +1,8 @@
1
+ import atexit
1
2
  import functools
3
+ import os
2
4
  import pathlib
5
+ import shutil
3
6
  import sys
4
7
  from typing import Dict, List, Optional, Tuple
5
8
 
@@ -201,7 +204,9 @@ def get_digest_as_string(
201
204
 
202
205
 
203
206
  def get_new_sandbox(root: pathlib.Path = pathlib.Path()) -> SandboxBase:
204
- return get_sandbox_type()(file_cacher=get_file_cacher(root), temp_dir=TEMP_DIR)
207
+ sandbox = get_sandbox_type()(file_cacher=get_file_cacher(root), temp_dir=TEMP_DIR)
208
+ atexit.register(lambda: sandbox.cleanup(delete=True))
209
+ return sandbox
205
210
 
206
211
 
207
212
  @functools.cache
@@ -209,6 +214,13 @@ def get_singleton_sandbox(root: pathlib.Path = pathlib.Path()) -> SandboxBase:
209
214
  return get_new_sandbox(root)
210
215
 
211
216
 
217
+ @functools.cache
218
+ def get_singleton_interactor_sandbox(
219
+ root: pathlib.Path = pathlib.Path(),
220
+ ) -> SandboxBase:
221
+ return get_new_sandbox(root)
222
+
223
+
212
224
  @functools.cache
213
225
  def get_build_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
214
226
  return find_problem(root) / 'build'
@@ -266,6 +278,23 @@ def get_checker(root: pathlib.Path = pathlib.Path()) -> CodeItem:
266
278
  )
267
279
 
268
280
 
281
+ @functools.cache
282
+ def get_interactor_or_nil(root: pathlib.Path = pathlib.Path()) -> Optional[CodeItem]:
283
+ package = find_problem_package_or_die(root)
284
+ return package.interactor
285
+
286
+
287
+ @functools.cache
288
+ def get_interactor(root: pathlib.Path = pathlib.Path()) -> CodeItem:
289
+ interactor = get_interactor_or_nil(root)
290
+ if interactor is None:
291
+ console.console.print(
292
+ '[error]Problem does not have an interactor configured.[/error]'
293
+ )
294
+ raise typer.Exit(1)
295
+ return interactor
296
+
297
+
269
298
  @functools.cache
270
299
  def get_solutions(root: pathlib.Path = pathlib.Path()) -> List[Solution]:
271
300
  package = find_problem_package_or_die(root)
@@ -371,6 +400,18 @@ def get_empty_sentinel_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
371
400
  return path
372
401
 
373
402
 
403
+ @functools.cache
404
+ def get_fifos(root: pathlib.Path = pathlib.Path()) -> Tuple[pathlib.Path, pathlib.Path]:
405
+ path = get_problem_cache_dir(root) / '.fifos'
406
+ shutil.rmtree(path, ignore_errors=True)
407
+ path.mkdir(parents=True, exist_ok=True)
408
+ fifo_in = path / 'fifo.in'
409
+ fifo_out = path / 'fifo.out'
410
+ os.mkfifo(fifo_in)
411
+ os.mkfifo(fifo_out)
412
+ return fifo_in, fifo_out
413
+
414
+
374
415
  def clear_package_cache():
375
416
  pkgs = [sys.modules[__name__]]
376
417
 
@@ -169,7 +169,8 @@ class BocaPackager(BasePackager):
169
169
  compile_text = compile_text.replace('{{rbxFlags}}', flags[language])
170
170
  return compile_text
171
171
 
172
- def _copy_solutions(self, into_path: pathlib.Path):
172
+ def _copy_solutions(self, into_path: pathlib.Path, fix_java: bool = True):
173
+ into_path = into_path / 'solutions'
173
174
  for solution in package.get_solutions():
174
175
  dest_path = (
175
176
  into_path
rbx/box/packaging/main.py CHANGED
@@ -2,6 +2,7 @@ import pathlib
2
2
  import tempfile
3
3
  from typing import Type
4
4
 
5
+ import syncer
5
6
  import typer
6
7
 
7
8
  from rbx import annotations, console
@@ -13,20 +14,21 @@ from rbx.box.statements.build_statements import build_statement
13
14
  app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
14
15
 
15
16
 
16
- def run_packager(
17
+ async def run_packager(
17
18
  packager_cls: Type[BasePackager],
18
19
  verification: environment.VerificationParam,
20
+ **kwargs,
19
21
  ) -> pathlib.Path:
20
22
  from rbx.box import builder
21
23
 
22
- if not builder.verify(verification=verification):
24
+ if not await builder.verify(verification=verification):
23
25
  console.console.print(
24
26
  '[error]Build or verification failed, check the report.[/error]'
25
27
  )
26
28
  raise typer.Exit(1)
27
29
 
28
30
  pkg = package.find_problem_package_or_die()
29
- packager = packager_cls()
31
+ packager = packager_cls(**kwargs)
30
32
 
31
33
  statement_types = packager.statement_types()
32
34
  built_statements = []
@@ -55,18 +57,33 @@ def run_packager(
55
57
 
56
58
 
57
59
  @app.command('polygon', help='Build a package for Polygon.')
58
- def polygon(
60
+ @syncer.sync
61
+ async def polygon(
59
62
  verification: environment.VerificationParam,
60
63
  ):
61
64
  from rbx.box.packaging.polygon.packager import PolygonPackager
62
65
 
63
- run_packager(PolygonPackager, verification=verification)
66
+ await run_packager(PolygonPackager, verification=verification)
64
67
 
65
68
 
66
69
  @app.command('boca', help='Build a package for BOCA.')
67
- def boca(
70
+ @syncer.sync
71
+ async def boca(
68
72
  verification: environment.VerificationParam,
69
73
  ):
70
74
  from rbx.box.packaging.boca.packager import BocaPackager
71
75
 
72
- run_packager(BocaPackager, verification=verification)
76
+ await run_packager(BocaPackager, verification=verification)
77
+
78
+
79
+ @app.command('moj', help='Build a package for MOJ.')
80
+ @syncer.sync
81
+ async def moj(
82
+ verification: environment.VerificationParam,
83
+ for_boca: bool = typer.Option(
84
+ False, help='Build a package for BOCA instead of MOJ.'
85
+ ),
86
+ ):
87
+ from rbx.box.packaging.moj.packager import MojPackager
88
+
89
+ await run_packager(MojPackager, verification=verification, for_boca=for_boca)
@@ -0,0 +1,164 @@
1
+ import pathlib
2
+ import shutil
3
+ from typing import List
4
+
5
+ import typer
6
+
7
+ from rbx import console
8
+ from rbx.box import package
9
+ from rbx.box.environment import get_extension_or_default
10
+ from rbx.box.packaging.boca.extension import BocaExtension
11
+ from rbx.box.packaging.boca.packager import BocaPackager
12
+ from rbx.box.packaging.packager import BuiltStatement
13
+ from rbx.box.schema import ExpectedOutcome
14
+ from rbx.config import get_default_app_path, get_testlib
15
+ from rbx.grading.judge.digester import digest_cooperatively
16
+
17
+
18
+ class MojPackager(BocaPackager):
19
+ def __init__(self, for_boca: bool = False):
20
+ super().__init__()
21
+ self.for_boca = for_boca
22
+
23
+ def _get_problem_info(self) -> str:
24
+ statement = self._get_main_statement()
25
+ return (
26
+ f'basename={self._get_problem_name()}\n'
27
+ f'fullname={statement.title}\n'
28
+ f'descfile={self._get_problem_name()}.pdf\n'
29
+ )
30
+
31
+ def _get_tl(self) -> str:
32
+ extension = get_extension_or_default('boca', BocaExtension)
33
+
34
+ pkg = package.find_problem_package_or_die()
35
+ res = f'TL[default]={pkg.timeLimit / 1000}\n'
36
+ for language in extension.languages:
37
+ res += f'TL[{language}]={self._get_pkg_timelimit(language) / 1000}\n'
38
+ return res
39
+
40
+ def _get_limits(self) -> str:
41
+ pkg = package.find_problem_package_or_die()
42
+ ml = pkg.memoryLimit
43
+ ol = pkg.outputLimit
44
+ return f'ULIMITS[-f]={ol}\n' f'ULIMITS[-v]={ml * 1024}\n'
45
+
46
+ def _get_compare(self) -> str:
47
+ extension = get_extension_or_default('boca', BocaExtension)
48
+
49
+ compare_path = (
50
+ get_default_app_path() / 'packagers' / 'moj' / 'scripts' / 'compare.sh'
51
+ )
52
+ if not compare_path.exists():
53
+ console.console.print(
54
+ '[error]MOJ template compare script not found.[/error]'
55
+ )
56
+ raise typer.Exit(1)
57
+ with package.get_checker().path.open('rb') as f:
58
+ checker_hash = digest_cooperatively(f)
59
+ return (
60
+ compare_path.read_text()
61
+ .replace('{{rbxFlags}}', extension.flags_with_defaults()['cc'])
62
+ .replace('{{checkerHash}}', checker_hash)
63
+ )
64
+
65
+ def _get_checker(self) -> str:
66
+ return package.get_checker().path.read_text()
67
+
68
+ def _copy_solutions_moj(self, into_path: pathlib.Path):
69
+ into_path = into_path / 'sols'
70
+ has_good = False
71
+ for solution in package.get_solutions():
72
+ tag = 'wrong'
73
+ if solution.outcome == ExpectedOutcome.ACCEPTED:
74
+ tag = 'good'
75
+ has_good = True
76
+ elif solution.outcome.is_slow():
77
+ tag = 'slow'
78
+ dest_path = into_path / tag / solution.path.name
79
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
80
+ shutil.copy(str(solution.path), dest_path)
81
+
82
+ if not has_good:
83
+ console.console.print('[error]No good solution found.[/error]')
84
+ raise typer.Exit(1)
85
+
86
+ def name(self) -> str:
87
+ return 'moj'
88
+
89
+ def package(
90
+ self,
91
+ build_path: pathlib.Path,
92
+ into_path: pathlib.Path,
93
+ built_statements: List[BuiltStatement],
94
+ ) -> pathlib.Path:
95
+ # Prepare dummy files
96
+ author_path = into_path / 'author'
97
+ author_path.parent.mkdir(parents=True, exist_ok=True)
98
+ author_path.write_text('Unknown\n')
99
+
100
+ tags_path = into_path / 'tags'
101
+ tags_path.parent.mkdir(parents=True, exist_ok=True)
102
+ tags_path.write_text('')
103
+
104
+ # Prepare limits
105
+ limits_path = into_path / 'conf'
106
+ limits_path.parent.mkdir(parents=True, exist_ok=True)
107
+ limits_path.write_text(self._get_limits())
108
+
109
+ # Prepare TL
110
+ if self.for_boca:
111
+ tl_path = into_path / 'tl'
112
+ tl_path.parent.mkdir(parents=True, exist_ok=True)
113
+ tl_path.write_text(self._get_tl())
114
+
115
+ # Prepare compare
116
+ compare_path = into_path / 'scripts' / 'compare.sh'
117
+ compare_path.parent.mkdir(parents=True, exist_ok=True)
118
+ compare_path.write_text(self._get_compare())
119
+ compare_path.chmod(0o755)
120
+
121
+ # Prepare testlib
122
+ testlib_path = into_path / 'scripts' / 'testlib.h'
123
+ testlib_path.parent.mkdir(parents=True, exist_ok=True)
124
+ testlib_path.write_text(get_testlib().read_text())
125
+
126
+ # Prepare checker
127
+ checker_path = into_path / 'scripts' / 'checker.cpp'
128
+ checker_path.parent.mkdir(parents=True, exist_ok=True)
129
+ checker_path.write_text(self._get_checker())
130
+
131
+ # Problem statement
132
+ enunciado_path = into_path / 'docs' / 'enunciado.pdf'
133
+ enunciado_path.parent.mkdir(parents=True, exist_ok=True)
134
+ shutil.copyfile(
135
+ self._get_main_built_statement(built_statements).path,
136
+ enunciado_path,
137
+ )
138
+
139
+ # Copy solutions
140
+ if self.for_boca:
141
+ self._copy_solutions(into_path, fix_java=False)
142
+ else:
143
+ self._copy_solutions_moj(into_path)
144
+
145
+ # Prepare IO
146
+ inputs_path = into_path / 'tests' / 'input'
147
+ inputs_path.mkdir(parents=True, exist_ok=True)
148
+ outputs_path = into_path / 'tests' / 'output'
149
+ outputs_path.mkdir(parents=True, exist_ok=True)
150
+
151
+ testcases = self.get_flattened_built_testcases()
152
+ for i, testcase in enumerate(testcases):
153
+ shutil.copyfile(testcase.inputPath, inputs_path / f'{i+1:03d}')
154
+ if testcase.outputPath is not None:
155
+ shutil.copyfile(testcase.outputPath, outputs_path / f'{i+1:03d}')
156
+ else:
157
+ (outputs_path / f'{i+1:03d}').touch()
158
+
159
+ # Zip all.
160
+ shutil.make_archive(
161
+ str(build_path / self._get_problem_name()), 'zip', into_path
162
+ )
163
+
164
+ return (build_path / self._get_problem_name()).with_suffix('.zip')
rbx/box/retries.py CHANGED
@@ -3,7 +3,7 @@ import pathlib
3
3
  import shutil
4
4
  import tempfile
5
5
  from contextlib import contextmanager
6
- from typing import Callable, List, Optional
6
+ from typing import Awaitable, Callable, List, Optional
7
7
 
8
8
  from rbx.box import package
9
9
  from rbx.box.setter_config import RepeatsConfig, get_setter_config
@@ -104,18 +104,18 @@ class Retrier:
104
104
  self.retries_for_stress = self.config.retries_for_stress
105
105
  self.retry_index = 0
106
106
 
107
- def repeat(
107
+ async def repeat(
108
108
  self,
109
- func: Callable[[int], Evaluation],
109
+ func: Callable[[int], Awaitable[Evaluation]],
110
110
  ) -> Evaluation:
111
111
  self.retry_index += 1
112
- eval = func(self.retry_index)
112
+ eval = await func(self.retry_index)
113
113
  if self.should_repeat(eval):
114
114
  with _temp_retry_dir() as temp_dir:
115
115
  # Move files to temp dir to open run for repeat.
116
116
  recover = _move_logs_to_temp_dir(eval, temp_dir)
117
117
  # Actually repeat and choose the best evaluation.
118
- next_eval = self.repeat(func)
118
+ next_eval = await self.repeat(func)
119
119
  chosen_eval = _merge_evaluations(eval, next_eval)
120
120
 
121
121
  if id(chosen_eval) == id(eval):
rbx/box/schema.py CHANGED
@@ -82,16 +82,16 @@ class ExpectedOutcome(AutoEnum):
82
82
  RUNTIME_ERROR = alias('runtime error', 'rte', 're') # type: ignore
83
83
  """Expected outcome solutions that finish with non-zero code (RTE)."""
84
84
 
85
- TIME_LIMIT_EXCEEDED = alias('time limit exceeded', 'timeout', 'tle') # type: ignore
85
+ TIME_LIMIT_EXCEEDED = alias('time limit exceeded', 'timeout', 'tle', 'tl') # type: ignore
86
86
  """Expected outcome for solutions that do not finish in time."""
87
87
 
88
- MEMORY_LIMIT_EXCEEDED = alias('memory limit exceeded', 'mle') # type: ignore
88
+ MEMORY_LIMIT_EXCEEDED = alias('memory limit exceeded', 'mle', 'ml') # type: ignore
89
89
  """Expected outcome for solutions that use more memory than allowed."""
90
90
 
91
- OUTPUT_LIMIT_EXCEEDED = alias('output limit exceeded', 'ole') # type: ignore
91
+ OUTPUT_LIMIT_EXCEEDED = alias('output limit exceeded', 'ole', 'ol') # type: ignore
92
92
  """Expected outcome for solutions that use more output than allowed."""
93
93
 
94
- TLE_OR_RTE = alias('tle or rte', 'tle/rte', 'tle+rte') # type: ignore
94
+ TLE_OR_RTE = alias('tle or rte', 'tle/rte', 'tle+rte', 'tle or re', 'tle+re') # type: ignore
95
95
  """Expected outcome for solutions that finish with either TLE or RTE.
96
96
 
97
97
  Especially useful for environments where TLE and RTE are indistinguishable."""
@@ -148,6 +148,22 @@ class ExpectedOutcome(AutoEnum):
148
148
  return bool(set(self.get_matches()) & set(rhs.get_matches()))
149
149
 
150
150
 
151
+ class ValidatorOutcome(AutoEnum):
152
+ VALID = alias('valid') # type: ignore
153
+ """Expected outcome for valid tests."""
154
+
155
+ INVALID = alias('invalid') # type: ignore
156
+ """Expected outcome for invalid tests."""
157
+
158
+
159
+ class TaskType(AutoEnum):
160
+ BATCH = alias('batch') # type: ignore
161
+ """Batch task."""
162
+
163
+ COMMUNICATION = alias('communication') # type: ignore
164
+ """Communication task."""
165
+
166
+
151
167
  class CodeItem(BaseModel):
152
168
  model_config = ConfigDict(extra='forbid')
153
169
 
@@ -337,12 +353,69 @@ class LimitModifiers(BaseModel):
337
353
  )
338
354
 
339
355
 
356
+ class ValidatorTest(BaseModel):
357
+ model_config = ConfigDict(extra='forbid')
358
+
359
+ input: pathlib.Path = Field(
360
+ description='The input file to be used as unit test input for the validator.'
361
+ )
362
+ outcome: ValidatorOutcome = Field(
363
+ default=ValidatorOutcome.VALID,
364
+ description='The expected outcome of the validator.',
365
+ )
366
+
367
+ validator: Optional[CodeItem] = Field(
368
+ default=None,
369
+ description='The validator to use for this test. If not specified, will use the package-level validator.',
370
+ )
371
+
372
+
373
+ class CheckerTest(BaseModel):
374
+ model_config = ConfigDict(extra='forbid')
375
+
376
+ input: Optional[pathlib.Path] = Field(
377
+ default=None,
378
+ description='The input file to be used as unit test input for the checker. If not specified, will pass an empty file.',
379
+ )
380
+ output: Optional[pathlib.Path] = Field(
381
+ default=None,
382
+ description='The solution output file to be used as unit test output for the checker. If not specified, will pass an empty file.',
383
+ )
384
+ answer: Optional[pathlib.Path] = Field(
385
+ default=None,
386
+ description='The answer file to be used as unit test answer for the checker. If not specified, will pass an empty file.',
387
+ )
388
+
389
+ outcome: ExpectedOutcome = Field(
390
+ default=ExpectedOutcome.ACCEPTED,
391
+ description='The expected outcome of the checker.',
392
+ )
393
+
394
+
395
+ class UnitTests(BaseModel):
396
+ model_config = ConfigDict(extra='forbid')
397
+
398
+ validator: List[ValidatorTest] = Field(
399
+ default=[],
400
+ description='Unit tests for the validator.',
401
+ )
402
+
403
+ checker: List[CheckerTest] = Field(
404
+ default=[],
405
+ description='Unit tests for the checker.',
406
+ )
407
+
408
+
340
409
  class Package(BaseModel):
341
410
  model_config = ConfigDict(extra='forbid')
342
411
 
343
412
  # Name of the problem.
344
413
  name: str = NameField(description='The name of the problem.')
345
414
 
415
+ type: TaskType = Field(
416
+ default=TaskType.BATCH, description='The type of the problem.'
417
+ )
418
+
346
419
  timeLimit: int = Field(description='Time limit of the problem, in milliseconds.')
347
420
 
348
421
  memoryLimit: int = Field(description='Memory limit of the problem, in MB.')
@@ -362,6 +435,10 @@ class Package(BaseModel):
362
435
  default=None, description='The checker for this problem.'
363
436
  )
364
437
 
438
+ interactor: Optional[CodeItem] = Field(
439
+ default=None, description='The interactor for this problem.'
440
+ )
441
+
365
442
  validator: Optional[CodeItem] = Field(
366
443
  default=None, description='The validator for this problem.'
367
444
  )
@@ -399,6 +476,11 @@ that is correct and used as reference -- and should have the `accepted` outcome.
399
476
  default={}, description='Variables to be re-used across the package.'
400
477
  )
401
478
 
479
+ unitTests: UnitTests = Field(
480
+ default_factory=UnitTests,
481
+ description='Unit tests for components of this problem.',
482
+ )
483
+
402
484
  @property
403
485
  def expanded_vars(self) -> Dict[str, Primitive]:
404
486
  return {key: expand_var(value) for key, value in self.vars.items()}