rbx.cp 0.13.8__py3-none-any.whl → 0.14.0__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/cli.py +74 -70
- rbx/box/code.py +3 -0
- rbx/box/contest/build_contest_statements.py +65 -23
- rbx/box/contest/contest_package.py +8 -1
- rbx/box/contest/main.py +9 -3
- rbx/box/contest/schema.py +17 -13
- rbx/box/contest/statements.py +12 -8
- rbx/box/dump_schemas.py +2 -1
- rbx/box/environment.py +1 -1
- rbx/box/fields.py +22 -4
- rbx/box/generators.py +32 -13
- rbx/box/limits_info.py +161 -0
- rbx/box/package.py +18 -1
- rbx/box/packaging/boca/boca_language_utils.py +26 -0
- rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
- rbx/box/packaging/boca/packager.py +7 -5
- rbx/box/packaging/contest_main.py +20 -12
- rbx/box/packaging/packager.py +24 -14
- rbx/box/packaging/polygon/packager.py +7 -3
- rbx/box/packaging/polygon/upload.py +2 -1
- rbx/box/presets/__init__.py +64 -64
- rbx/box/remote.py +3 -3
- rbx/box/sanitizers/issue_stack.py +124 -0
- rbx/box/schema.py +87 -27
- rbx/box/solutions.py +74 -117
- rbx/box/statements/build_statements.py +12 -1
- rbx/box/statements/builders.py +5 -3
- rbx/box/statements/latex_jinja.py +73 -23
- rbx/box/statements/schema.py +7 -9
- rbx/box/stressing/generator_parser.py +3 -1
- rbx/box/tasks.py +10 -10
- rbx/box/testcase_extractors.py +8 -0
- rbx/box/testing/testing_preset.py +129 -2
- rbx/box/testing/testing_shared.py +3 -1
- rbx/box/timing.py +305 -0
- rbx/box/tooling/boca/debug_utils.py +88 -0
- rbx/box/tooling/boca/manual_scrape.py +20 -0
- rbx/box/tooling/boca/scraper.py +660 -57
- rbx/box/unit.py +0 -2
- rbx/box/validators.py +0 -4
- rbx/grading/judge/cacher.py +36 -0
- rbx/grading/judge/program.py +12 -2
- rbx/grading/judge/sandbox.py +1 -1
- rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
- rbx/grading/judge/storage.py +36 -3
- rbx/grading/limits.py +4 -0
- rbx/grading/steps.py +3 -2
- rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
- rbx/resources/presets/default/contest/statement/info.rbx.tex +54 -0
- rbx/resources/presets/default/problem/.gitignore +1 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
- rbx/resources/presets/default/problem/rbx.h +52 -5
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
- rbx/resources/presets/default/problem/testlib.h +6299 -0
- rbx/resources/presets/default/problem/validator.cpp +4 -3
- rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
- rbx/resources/presets/default/shared/icpc.sty +16 -1
- rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
- rbx/testing_utils.py +17 -1
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/METADATA +4 -2
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/RECORD +65 -62
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/WHEEL +1 -1
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/entry_points.txt +0 -1
- rbx/providers/__init__.py +0 -43
- rbx/providers/codeforces.py +0 -73
- rbx/providers/provider.py +0 -26
- rbx/submitors/__init__.py +0 -18
- rbx/submitors/codeforces.py +0 -121
- rbx/submitors/submitor.py +0 -25
- /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
- {rbx_cp-0.13.8.dist-info → rbx_cp-0.14.0.dist-info}/LICENSE +0 -0
@@ -6,10 +6,11 @@ with Latex.
|
|
6
6
|
import pathlib
|
7
7
|
import re
|
8
8
|
import typing
|
9
|
-
from typing import Any, Dict, Tuple, Union
|
9
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
10
10
|
|
11
11
|
import jinja2
|
12
12
|
import jinja2.runtime
|
13
|
+
import rich.pretty
|
13
14
|
import typer
|
14
15
|
|
15
16
|
from rbx import console
|
@@ -188,17 +189,62 @@ class StrictChainableUndefined(jinja2.StrictUndefined):
|
|
188
189
|
return self
|
189
190
|
|
190
191
|
|
192
|
+
class VarWrapperUndefinedError(jinja2.UndefinedError):
|
193
|
+
def __init__(self, *args, **kwargs):
|
194
|
+
super().__init__(*args, **kwargs)
|
195
|
+
|
196
|
+
def vars(self) -> Dict[str, Any]:
|
197
|
+
return {}
|
198
|
+
|
199
|
+
|
191
200
|
class JinjaDictWrapper(dict):
|
192
|
-
def __init__(
|
201
|
+
def __init__(
|
202
|
+
self,
|
203
|
+
*args,
|
204
|
+
key='dict object',
|
205
|
+
prefix='',
|
206
|
+
**kwargs,
|
207
|
+
):
|
193
208
|
super().__init__(*args, **kwargs)
|
194
209
|
self.key = key
|
195
|
-
|
196
|
-
|
210
|
+
self.prefix = prefix
|
211
|
+
self.ancestor_d: Optional[Dict[str, Any]] = None
|
212
|
+
|
213
|
+
slf = self
|
214
|
+
|
215
|
+
class _AccessError(VarWrapperUndefinedError):
|
216
|
+
def __init__(self, *args, **kwargs):
|
217
|
+
super().__init__(*args, **kwargs)
|
218
|
+
|
219
|
+
def vars(self) -> Dict[str, Any]:
|
220
|
+
return slf.ancestor_d or slf
|
221
|
+
|
222
|
+
self.exc = _AccessError
|
223
|
+
|
224
|
+
@classmethod
|
225
|
+
def from_dict(cls, d: Dict[str, Any], wrapper_key: str) -> 'JinjaDictWrapper':
|
226
|
+
res = cls(key=wrapper_key)
|
227
|
+
for key, value in d.items():
|
228
|
+
splits = key.split('.')
|
229
|
+
prefix = ''
|
230
|
+
acc = res
|
231
|
+
for split in splits[:-1]:
|
232
|
+
prefix = f'{prefix}.{split}'.strip('.')
|
233
|
+
if split not in acc or not isinstance(acc[split], dict):
|
234
|
+
acc[split] = JinjaDictWrapper(key=wrapper_key, prefix=prefix)
|
235
|
+
acc[split].ancestor_d = res
|
236
|
+
acc = acc[split]
|
237
|
+
acc[splits[-1]] = value
|
238
|
+
return res
|
239
|
+
|
240
|
+
def __getitem__(self, key: str) -> Any:
|
197
241
|
try:
|
198
242
|
return super().__getitem__(key)
|
199
243
|
except KeyError:
|
244
|
+
final_key = f'{self.prefix}.{key}'.strip('.')
|
200
245
|
return StrictChainableUndefined(
|
201
|
-
hint=f'"{
|
246
|
+
hint=f'"{final_key}" was not found in "{self.key}"',
|
247
|
+
exc=self.exc,
|
202
248
|
)
|
203
249
|
|
204
250
|
|
@@ -216,6 +262,22 @@ def add_builtin_tests(j2_env: jinja2.Environment):
|
|
216
262
|
j2_env.tests['nonnull'] = test_var_nonnull
|
217
263
|
|
218
264
|
|
265
|
+
def _handle_rendering_undefined(
|
266
|
+
err: jinja2.UndefinedError,
|
267
|
+
) -> str:
|
268
|
+
console.console.print('[error]Error while rendering Jinja2 template:', end=' ')
|
269
|
+
console.console.print(err)
|
270
|
+
console.console.print(
|
271
|
+
'[warning]This usually happens when accessing an undefined variable.[/warning]'
|
272
|
+
)
|
273
|
+
if isinstance(err, VarWrapperUndefinedError):
|
274
|
+
vars = err.vars()
|
275
|
+
if vars:
|
276
|
+
console.console.print('[warning]Defined variables are[/warning] ', end='')
|
277
|
+
console.console.print(rich.pretty.Pretty(vars))
|
278
|
+
raise typer.Abort() from err
|
279
|
+
|
280
|
+
|
219
281
|
def render_latex_template(path_templates, template_filename, template_vars=None) -> str:
|
220
282
|
"""Render a latex template, filling in its template variables
|
221
283
|
|
@@ -237,12 +299,8 @@ def render_latex_template(path_templates, template_filename, template_vars=None)
|
|
237
299
|
try:
|
238
300
|
return template.render(**var_dict) # type: ignore
|
239
301
|
except jinja2.UndefinedError as err:
|
240
|
-
|
241
|
-
|
242
|
-
console.console.print(
|
243
|
-
'[warning]This usually happens when accessing an undefined variable.[/warning]'
|
244
|
-
)
|
245
|
-
raise typer.Abort() from err
|
302
|
+
_handle_rendering_undefined(err)
|
303
|
+
raise
|
246
304
|
|
247
305
|
|
248
306
|
def render_latex_template_blocks(
|
@@ -269,12 +327,8 @@ def render_latex_template_blocks(
|
|
269
327
|
try:
|
270
328
|
return {key: ''.join(value(ctx)) for key, value in template.blocks.items()}
|
271
329
|
except jinja2.UndefinedError as err:
|
272
|
-
|
273
|
-
|
274
|
-
console.console.print(
|
275
|
-
'[warning]This usually happens when accessing an undefined variable.[/warning]'
|
276
|
-
)
|
277
|
-
raise typer.Abort() from err
|
330
|
+
_handle_rendering_undefined(err)
|
331
|
+
raise
|
278
332
|
|
279
333
|
|
280
334
|
def render_markdown_template_blocks(
|
@@ -301,9 +355,5 @@ def render_markdown_template_blocks(
|
|
301
355
|
try:
|
302
356
|
return {key: ''.join(value(ctx)) for key, value in template.blocks.items()}
|
303
357
|
except jinja2.UndefinedError as err:
|
304
|
-
|
305
|
-
|
306
|
-
console.console.print(
|
307
|
-
'[warning]This usually happens when accessing an undefined variable.[/warning]'
|
308
|
-
)
|
309
|
-
raise typer.Abort() from err
|
358
|
+
_handle_rendering_undefined(err)
|
359
|
+
raise
|
rbx/box/statements/schema.py
CHANGED
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import pathlib
|
4
4
|
from enum import Enum
|
5
|
-
from typing import Annotated,
|
5
|
+
from typing import Annotated, List, Literal, Optional, Union
|
6
6
|
|
7
7
|
from pydantic import AfterValidator, BaseModel, ConfigDict, Field
|
8
8
|
|
9
9
|
from rbx.autoenum import AutoEnum, alias
|
10
|
-
from rbx.box.fields import FNameField,
|
10
|
+
from rbx.box.fields import FNameField, RecVars, Vars, expand_vars
|
11
11
|
from rbx.box.lang import is_valid_lang_code
|
12
12
|
|
13
13
|
|
@@ -144,9 +144,8 @@ class Statement(BaseModel):
|
|
144
144
|
default=StatementType.rbxTeX, description='Type of the input statement file.'
|
145
145
|
)
|
146
146
|
|
147
|
-
steps: List[ConversionStep] = Field(
|
147
|
+
steps: List[Annotated[ConversionStep, Field(discriminator='type')]] = Field(
|
148
148
|
default=[],
|
149
|
-
discriminator='type',
|
150
149
|
description="""
|
151
150
|
Describes a sequence of conversion steps that should be applied to the statement file.
|
152
151
|
|
@@ -156,9 +155,8 @@ certain conversion steps to happen.
|
|
156
155
|
""",
|
157
156
|
)
|
158
157
|
|
159
|
-
configure: List[ConversionStep] = Field(
|
158
|
+
configure: List[Annotated[ConversionStep, Field(discriminator='type')]] = Field(
|
160
159
|
default=[],
|
161
|
-
discriminator='type',
|
162
160
|
description="""
|
163
161
|
Configure how certain conversion steps should happen when applied to the statement file.
|
164
162
|
|
@@ -176,11 +174,11 @@ their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
|
|
176
174
|
""",
|
177
175
|
)
|
178
176
|
|
179
|
-
vars:
|
177
|
+
vars: RecVars = Field(
|
180
178
|
default={},
|
181
179
|
description='Variables to be used in the statement.',
|
182
180
|
)
|
183
181
|
|
184
182
|
@property
|
185
|
-
def expanded_vars(self) ->
|
186
|
-
return
|
183
|
+
def expanded_vars(self) -> Vars:
|
184
|
+
return expand_vars(self.vars)
|
@@ -24,7 +24,7 @@ _expr: var | range | select
|
|
24
24
|
_ticked: "`" _expr "`"
|
25
25
|
|
26
26
|
// Variables
|
27
|
-
var: "<"
|
27
|
+
var: "<" RECNAME ">"
|
28
28
|
|
29
29
|
// Select
|
30
30
|
select: "(" select_value ("|" select_value)* ")"
|
@@ -46,6 +46,8 @@ TEXT: (/[^ \t\f\r\n\[\]\(\)\<\>\|\`]/ | ESCAPED_STRING)+
|
|
46
46
|
// Whitespace
|
47
47
|
_WS: WS
|
48
48
|
|
49
|
+
RECNAME: /[a-zA-Z0-9_]/+ /(\.[a-zA-Z0-9_])/*
|
50
|
+
|
49
51
|
%import common.WS
|
50
52
|
%import common.CNAME
|
51
53
|
%import common.SIGNED_INT
|
rbx/box/tasks.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import pathlib
|
2
2
|
from typing import Optional
|
3
3
|
|
4
|
-
from rbx.box import checkers, package, state
|
4
|
+
from rbx.box import checkers, limits_info, package, state
|
5
5
|
from rbx.box.code import CommunicationItem, run_communication, run_item
|
6
6
|
from rbx.box.environment import EnvironmentSandbox, ExecutionConfig, VerificationLevel
|
7
7
|
from rbx.box.retries import Retrier, get_retrier_config
|
@@ -27,16 +27,16 @@ def get_limits_for_language(
|
|
27
27
|
timelimit_override: Optional[int],
|
28
28
|
use_timelimit: bool = True,
|
29
29
|
) -> Limits:
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
return Limits(
|
35
|
-
time=time if use_timelimit and time > 0 else None,
|
36
|
-
memory=memory,
|
37
|
-
output=pkg.outputLimit,
|
38
|
-
isDoubleTL=isDoubleTL,
|
30
|
+
limits = limits_info.get_limits(
|
31
|
+
lang,
|
32
|
+
profile=limits_info.get_active_profile() or 'local',
|
33
|
+
verification=verification,
|
39
34
|
)
|
35
|
+
if timelimit_override is not None:
|
36
|
+
limits.time = timelimit_override
|
37
|
+
if limits.time is not None and (not use_timelimit or limits.time <= 0):
|
38
|
+
limits.time = None
|
39
|
+
return limits
|
40
40
|
|
41
41
|
|
42
42
|
async def run_solution_on_testcase(
|
rbx/box/testcase_extractors.py
CHANGED
@@ -115,6 +115,7 @@ class GenerationTestcaseEntry(BaseModel):
|
|
115
115
|
metadata: GenerationMetadata
|
116
116
|
validator: Optional[CodeItem] = None
|
117
117
|
extra_validators: List[CodeItem] = []
|
118
|
+
model_solution: Optional[CodeItem] = None
|
118
119
|
|
119
120
|
|
120
121
|
class TestcaseVisitor(abc.ABC):
|
@@ -151,6 +152,7 @@ async def run_testcase_visitor(visitor: TestcaseVisitor):
|
|
151
152
|
prefix: List[str],
|
152
153
|
validator: Optional[CodeItem] = None,
|
153
154
|
extra_validators: Optional[List[CodeItem]] = None,
|
155
|
+
model_solution: Optional[CodeItem] = None,
|
154
156
|
):
|
155
157
|
extra_validators = extra_validators or []
|
156
158
|
|
@@ -192,6 +194,7 @@ async def run_testcase_visitor(visitor: TestcaseVisitor):
|
|
192
194
|
),
|
193
195
|
validator=validator,
|
194
196
|
extra_validators=extra_validators,
|
197
|
+
model_solution=model_solution,
|
195
198
|
)
|
196
199
|
)
|
197
200
|
i += 1
|
@@ -215,6 +218,7 @@ async def run_testcase_visitor(visitor: TestcaseVisitor):
|
|
215
218
|
),
|
216
219
|
validator=validator,
|
217
220
|
extra_validators=extra_validators,
|
221
|
+
model_solution=model_solution,
|
218
222
|
)
|
219
223
|
)
|
220
224
|
i += 1
|
@@ -231,6 +235,7 @@ async def run_testcase_visitor(visitor: TestcaseVisitor):
|
|
231
235
|
),
|
232
236
|
validator=validator,
|
233
237
|
extra_validators=extra_validators,
|
238
|
+
model_solution=model_solution,
|
234
239
|
)
|
235
240
|
)
|
236
241
|
i += 1
|
@@ -259,6 +264,7 @@ async def run_testcase_visitor(visitor: TestcaseVisitor):
|
|
259
264
|
),
|
260
265
|
validator=validator,
|
261
266
|
extra_validators=extra_validators,
|
267
|
+
model_solution=model_solution,
|
262
268
|
)
|
263
269
|
)
|
264
270
|
i += 1
|
@@ -278,6 +284,7 @@ async def run_testcase_visitor(visitor: TestcaseVisitor):
|
|
278
284
|
[group.name],
|
279
285
|
validator=group_validator,
|
280
286
|
extra_validators=extra_validators,
|
287
|
+
model_solution=group.model_solution,
|
281
288
|
)
|
282
289
|
|
283
290
|
for i, subgroup in enumerate(group.subgroups):
|
@@ -287,6 +294,7 @@ async def run_testcase_visitor(visitor: TestcaseVisitor):
|
|
287
294
|
[group.name, subgroup.name],
|
288
295
|
validator=group_validator,
|
289
296
|
extra_validators=extra_validators + subgroup.extraValidators,
|
297
|
+
model_solution=group.model_solution,
|
290
298
|
)
|
291
299
|
|
292
300
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import pathlib
|
2
2
|
|
3
3
|
from rbx import utils
|
4
|
-
from rbx.box.presets.schema import Preset
|
4
|
+
from rbx.box.presets.schema import Preset, TrackedAsset, Tracking
|
5
5
|
from rbx.box.testing.testing_shared import PathOrStr, TestingShared
|
6
6
|
|
7
7
|
|
@@ -16,13 +16,19 @@ class TestingPreset(TestingShared):
|
|
16
16
|
self.yml_path.touch()
|
17
17
|
self.yml_path.write_text(
|
18
18
|
utils.model_to_yaml(
|
19
|
-
Preset(
|
19
|
+
Preset(
|
20
|
+
name='test-preset',
|
21
|
+
uri='rsalesc/test-preset',
|
22
|
+
env=pathlib.Path('env.rbx.yml'),
|
23
|
+
tracking=Tracking(), # Explicitly include tracking
|
24
|
+
)
|
20
25
|
)
|
21
26
|
)
|
22
27
|
self.add_from_resources(
|
23
28
|
pathlib.Path('env.rbx.yml'), pathlib.Path('presets/default/env.rbx.yml')
|
24
29
|
)
|
25
30
|
|
31
|
+
@property
|
26
32
|
def yml_path(self) -> pathlib.Path:
|
27
33
|
return self.root / 'preset.rbx.yml'
|
28
34
|
|
@@ -34,3 +40,124 @@ class TestingPreset(TestingShared):
|
|
34
40
|
|
35
41
|
def save(self):
|
36
42
|
self.yml_path.write_text(utils.model_to_yaml(self.yml))
|
43
|
+
self._yml = None
|
44
|
+
|
45
|
+
def set_name(self, name: str):
|
46
|
+
"""Set the preset name."""
|
47
|
+
self.yml.name = name
|
48
|
+
self.save()
|
49
|
+
|
50
|
+
def set_uri(self, uri: str):
|
51
|
+
"""Set the preset URI."""
|
52
|
+
self.yml.uri = uri
|
53
|
+
self.save()
|
54
|
+
|
55
|
+
def set_env(self, env_path: PathOrStr):
|
56
|
+
"""Set the environment file path."""
|
57
|
+
self.yml.env = pathlib.Path(env_path)
|
58
|
+
self.save()
|
59
|
+
|
60
|
+
def set_problem_path(self, path: PathOrStr):
|
61
|
+
"""Set the problem package path."""
|
62
|
+
self.yml.problem = pathlib.Path(path)
|
63
|
+
self.save()
|
64
|
+
|
65
|
+
def set_contest_path(self, path: PathOrStr):
|
66
|
+
"""Set the contest package path."""
|
67
|
+
self.yml.contest = pathlib.Path(path)
|
68
|
+
self.save()
|
69
|
+
|
70
|
+
def add_problem_tracked_asset(self, path: PathOrStr, symlink: bool = False):
|
71
|
+
"""Add a tracked asset to the problem tracking list."""
|
72
|
+
# Create a new tracking object with the updated problem list
|
73
|
+
current_tracking = self.yml.tracking
|
74
|
+
new_problem_list = current_tracking.problem + [
|
75
|
+
TrackedAsset(path=pathlib.Path(path), symlink=symlink)
|
76
|
+
]
|
77
|
+
self.yml.tracking = Tracking(
|
78
|
+
problem=new_problem_list, contest=current_tracking.contest
|
79
|
+
)
|
80
|
+
self.save()
|
81
|
+
|
82
|
+
def add_contest_tracked_asset(self, path: PathOrStr, symlink: bool = False):
|
83
|
+
"""Add a tracked asset to the contest tracking list."""
|
84
|
+
# Create a new tracking object with the updated contest list
|
85
|
+
current_tracking = self.yml.tracking
|
86
|
+
new_contest_list = current_tracking.contest + [
|
87
|
+
TrackedAsset(path=pathlib.Path(path), symlink=symlink)
|
88
|
+
]
|
89
|
+
self.yml.tracking = Tracking(
|
90
|
+
problem=current_tracking.problem, contest=new_contest_list
|
91
|
+
)
|
92
|
+
self.save()
|
93
|
+
|
94
|
+
def create_problem_package(self):
|
95
|
+
"""Create a basic problem package structure."""
|
96
|
+
if self.yml.problem:
|
97
|
+
problem_dir = self.root / self.yml.problem
|
98
|
+
problem_dir.mkdir(parents=True, exist_ok=True)
|
99
|
+
|
100
|
+
# Create a basic problem.rbx.yml
|
101
|
+
problem_yml = problem_dir / 'problem.rbx.yml'
|
102
|
+
if not problem_yml.exists():
|
103
|
+
problem_yml.write_text("""---
|
104
|
+
name: "test-problem"
|
105
|
+
timeLimit: 1000
|
106
|
+
memoryLimit: 256
|
107
|
+
""")
|
108
|
+
|
109
|
+
def create_contest_package(self):
|
110
|
+
"""Create a basic contest package structure."""
|
111
|
+
if self.yml.contest:
|
112
|
+
contest_dir = self.root / self.yml.contest
|
113
|
+
contest_dir.mkdir(parents=True, exist_ok=True)
|
114
|
+
|
115
|
+
# Create a basic contest.rbx.yml
|
116
|
+
contest_yml = contest_dir / 'contest.rbx.yml'
|
117
|
+
if not contest_yml.exists():
|
118
|
+
contest_yml.write_text("""---
|
119
|
+
name: "Test Contest"
|
120
|
+
duration: 180
|
121
|
+
""")
|
122
|
+
|
123
|
+
def create_symlink(self, link_path: PathOrStr, target_path: PathOrStr):
|
124
|
+
"""Create a symlink from link_path to target_path relative to the preset root."""
|
125
|
+
link = self.root / link_path
|
126
|
+
target = pathlib.Path(target_path)
|
127
|
+
|
128
|
+
# Ensure parent directory exists
|
129
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
130
|
+
|
131
|
+
# Remove existing file/symlink if present
|
132
|
+
if link.exists() or link.is_symlink():
|
133
|
+
link.unlink()
|
134
|
+
|
135
|
+
# Create symlink
|
136
|
+
link.symlink_to(target)
|
137
|
+
|
138
|
+
def verify_file_exists(self, path: PathOrStr) -> bool:
|
139
|
+
"""Verify that a file exists in the preset."""
|
140
|
+
return (self.root / path).exists()
|
141
|
+
|
142
|
+
def verify_symlink(self, path: PathOrStr, expected_target: PathOrStr) -> bool:
|
143
|
+
"""Verify that a symlink exists and points to the expected target."""
|
144
|
+
link = self.root / path
|
145
|
+
if not link.is_symlink():
|
146
|
+
return False
|
147
|
+
return link.readlink() == pathlib.Path(expected_target)
|
148
|
+
|
149
|
+
def get_file_content(self, path: PathOrStr) -> str:
|
150
|
+
"""Get the content of a file in the preset."""
|
151
|
+
return (self.root / path).read_text()
|
152
|
+
|
153
|
+
def get_problem_dir(self) -> pathlib.Path:
|
154
|
+
"""Get the problem package directory."""
|
155
|
+
if not self.yml.problem:
|
156
|
+
raise ValueError('No problem package defined in preset')
|
157
|
+
return self.root / self.yml.problem
|
158
|
+
|
159
|
+
def get_contest_dir(self) -> pathlib.Path:
|
160
|
+
"""Get the contest package directory."""
|
161
|
+
if not self.yml.contest:
|
162
|
+
raise ValueError('No contest package defined in preset')
|
163
|
+
return self.root / self.yml.contest
|
@@ -28,7 +28,9 @@ class TestingShared:
|
|
28
28
|
os.chdir(self._old_cwd)
|
29
29
|
self.cleanup()
|
30
30
|
|
31
|
-
def path(self, path: PathOrStr) -> pathlib.Path:
|
31
|
+
def path(self, path: Optional[PathOrStr] = None) -> pathlib.Path:
|
32
|
+
if path is None:
|
33
|
+
return self.root
|
32
34
|
return self.root / path
|
33
35
|
|
34
36
|
def abspath(self, path: PathOrStr) -> pathlib.Path:
|