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.
- rbx/box/builder.py +6 -6
- rbx/box/checkers.py +105 -26
- rbx/box/cli.py +860 -0
- rbx/box/code.py +199 -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 -852
- rbx/box/package.py +42 -1
- rbx/box/packaging/boca/packager.py +2 -1
- rbx/box/packaging/main.py +24 -7
- rbx/box/packaging/moj/packager.py +164 -0
- rbx/box/retries.py +5 -5
- rbx/box/schema.py +86 -4
- rbx/box/solutions.py +46 -108
- rbx/box/solutions_test.py +5 -6
- rbx/box/statements/build_statements.py +4 -2
- rbx/box/stresses.py +23 -12
- rbx/box/tasks.py +258 -0
- rbx/box/testcase_extractors.py +21 -21
- rbx/box/testcases/main.py +19 -14
- rbx/box/unit.py +116 -0
- rbx/box/validators.py +27 -18
- rbx/box/validators_test.py +3 -3
- rbx/grading/judge/sandbox.py +8 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +12 -7
- rbx/grading/judge/sandboxes/timeit.py +8 -2
- 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/resources/packagers/moj/scripts/compare.sh +101 -0
- rbx/test.py +6 -4
- rbx/testdata/interactive/checker.cpp +21 -0
- rbx/testdata/interactive/gen.cpp +11 -0
- rbx/testdata/interactive/interactor.cpp +63 -0
- rbx/testdata/interactive/problem.rbx.yml +40 -0
- rbx/testdata/interactive/sols/af_ac_pe.cpp +75 -0
- rbx/testdata/interactive/sols/af_ac_re.cpp +76 -0
- rbx/testdata/interactive/sols/af_ac_too_many_iter.cpp +72 -0
- rbx/testdata/interactive/sols/af_inf_cout_with_flush.cpp +79 -0
- rbx/testdata/interactive/sols/af_inf_cout_without_flush.cpp +78 -0
- rbx/testdata/interactive/sols/af_ml.cpp +78 -0
- rbx/testdata/interactive/sols/af_tl_after_ans.cpp +74 -0
- rbx/testdata/interactive/sols/af_wa.cpp +74 -0
- rbx/testdata/interactive/sols/interactive-binary-search_mm_naive_cin.cpp +17 -0
- rbx/testdata/interactive/sols/main.cpp +26 -0
- rbx/testdata/interactive/testplan.txt +6 -0
- rbx/testdata/interactive/validator.cpp +16 -0
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/METADATA +2 -1
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/RECORD +53 -32
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.39.dist-info → rbx_cp-0.5.42.dist-info}/WHEEL +0 -0
- {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
|
-
|
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
|
-
|
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
|
-
|
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()}
|