rbx.cp 0.13.7__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.
Files changed (72) hide show
  1. rbx/box/cli.py +74 -70
  2. rbx/box/code.py +3 -0
  3. rbx/box/contest/build_contest_statements.py +65 -23
  4. rbx/box/contest/contest_package.py +8 -1
  5. rbx/box/contest/main.py +9 -3
  6. rbx/box/contest/schema.py +17 -13
  7. rbx/box/contest/statements.py +12 -8
  8. rbx/box/dump_schemas.py +2 -1
  9. rbx/box/environment.py +1 -1
  10. rbx/box/fields.py +22 -4
  11. rbx/box/generators.py +32 -13
  12. rbx/box/limits_info.py +161 -0
  13. rbx/box/package.py +18 -1
  14. rbx/box/packaging/boca/boca_language_utils.py +26 -0
  15. rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
  16. rbx/box/packaging/boca/packager.py +7 -5
  17. rbx/box/packaging/contest_main.py +20 -12
  18. rbx/box/packaging/packager.py +24 -14
  19. rbx/box/packaging/polygon/packager.py +7 -3
  20. rbx/box/packaging/polygon/upload.py +9 -2
  21. rbx/box/presets/__init__.py +64 -64
  22. rbx/box/remote.py +3 -3
  23. rbx/box/sanitizers/issue_stack.py +124 -0
  24. rbx/box/schema.py +87 -27
  25. rbx/box/solutions.py +74 -117
  26. rbx/box/statements/build_statements.py +12 -1
  27. rbx/box/statements/builders.py +5 -3
  28. rbx/box/statements/latex_jinja.py +73 -23
  29. rbx/box/statements/schema.py +7 -9
  30. rbx/box/stressing/generator_parser.py +3 -1
  31. rbx/box/tasks.py +10 -10
  32. rbx/box/testcase_extractors.py +8 -0
  33. rbx/box/testcase_utils.py +7 -7
  34. rbx/box/testing/testing_preset.py +129 -2
  35. rbx/box/testing/testing_shared.py +3 -1
  36. rbx/box/timing.py +305 -0
  37. rbx/box/tooling/boca/debug_utils.py +88 -0
  38. rbx/box/tooling/boca/manual_scrape.py +20 -0
  39. rbx/box/tooling/boca/scraper.py +660 -57
  40. rbx/box/unit.py +0 -2
  41. rbx/box/validators.py +0 -4
  42. rbx/grading/judge/cacher.py +36 -0
  43. rbx/grading/judge/program.py +12 -2
  44. rbx/grading/judge/sandbox.py +1 -1
  45. rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
  46. rbx/grading/judge/storage.py +36 -3
  47. rbx/grading/limits.py +4 -0
  48. rbx/grading/steps.py +3 -2
  49. rbx/resources/presets/default/contest/contest.rbx.yml +11 -1
  50. rbx/resources/presets/default/contest/statement/info.rbx.tex +54 -0
  51. rbx/resources/presets/default/problem/.gitignore +1 -0
  52. rbx/resources/presets/default/problem/problem.rbx.yml +21 -3
  53. rbx/resources/presets/default/problem/rbx.h +52 -5
  54. rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
  55. rbx/resources/presets/default/problem/testlib.h +6299 -0
  56. rbx/resources/presets/default/problem/validator.cpp +4 -3
  57. rbx/resources/presets/default/shared/contest_template.rbx.tex +13 -3
  58. rbx/resources/presets/default/shared/icpc.sty +33 -5
  59. rbx/resources/presets/default/shared/problem_template.rbx.tex +10 -1
  60. rbx/testing_utils.py +17 -1
  61. {rbx_cp-0.13.7.dist-info → rbx_cp-0.14.0.dist-info}/METADATA +4 -2
  62. {rbx_cp-0.13.7.dist-info → rbx_cp-0.14.0.dist-info}/RECORD +66 -63
  63. {rbx_cp-0.13.7.dist-info → rbx_cp-0.14.0.dist-info}/WHEEL +1 -1
  64. {rbx_cp-0.13.7.dist-info → rbx_cp-0.14.0.dist-info}/entry_points.txt +0 -1
  65. rbx/providers/__init__.py +0 -43
  66. rbx/providers/codeforces.py +0 -73
  67. rbx/providers/provider.py +0 -26
  68. rbx/submitors/__init__.py +0 -18
  69. rbx/submitors/codeforces.py +0 -121
  70. rbx/submitors/submitor.py +0 -25
  71. /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
  72. {rbx_cp-0.13.7.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__(self, *args, key='dict object', **kwargs):
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
- def __getitem__(self, key):
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'"{key}" was not found in "{self.key}"'
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
- console.console.print('[error]Error while rendering Jinja2 template:', end=' ')
241
- console.console.print(err)
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
- console.console.print('[error]Error while rendering Jinja2 template:', end=' ')
273
- console.console.print(err)
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
- console.console.print('[error]Error while rendering Jinja2 template:', end=' ')
305
- console.console.print(err)
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
@@ -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, Dict, List, Literal, Optional, Union
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, Primitive, expand_var
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: Dict[str, Primitive] = Field(
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) -> Dict[str, Primitive]:
186
- return {key: expand_var(value) for key, value in self.vars.items()}
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: "<" CNAME ">"
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
- pkg = package.find_problem_package_or_die()
31
- time = timelimit_override or pkg.timelimit_for_language(lang)
32
- isDoubleTL = verification.value >= VerificationLevel.FULL.value
33
- memory = pkg.memorylimit_for_language(lang)
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(
@@ -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
 
rbx/box/testcase_utils.py CHANGED
@@ -181,10 +181,10 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
181
181
 
182
182
  while line := f.readline().strip():
183
183
  if line.startswith(interactor_prefix):
184
- stripped = line[len(interactor_prefix) :].strip()
184
+ stripped = line[len(interactor_prefix) :].rstrip()
185
185
  entries.append(TestcaseInteractionEntry(data=stripped, pipe=0))
186
186
  elif line.startswith(solution_prefix):
187
- stripped = line[len(solution_prefix) :].strip()
187
+ stripped = line[len(solution_prefix) :].rstrip()
188
188
  entries.append(TestcaseInteractionEntry(data=stripped, pipe=1))
189
189
  else:
190
190
  raise TestcaseInteractionParsingError(
@@ -204,11 +204,11 @@ def get_alternate_interaction_texts(
204
204
  solution_entries = []
205
205
  for entry in interaction.entries:
206
206
  if entry.pipe == 1:
207
- solution_entries.append(entry.data)
208
- interactor_entries.extend(['\n'] * entry.data.count('\n'))
207
+ solution_entries.append(entry.data + '\n')
208
+ interactor_entries.extend(['\n'] * (entry.data.count('\n') + 1))
209
209
  else:
210
- interactor_entries.append(entry.data)
211
- solution_entries.extend(['\n'] * entry.data.count('\n'))
210
+ interactor_entries.append(entry.data + '\n')
211
+ solution_entries.extend(['\n'] * (entry.data.count('\n') + 1))
212
212
  return ''.join(interactor_entries), ''.join(solution_entries)
213
213
 
214
214
 
@@ -219,4 +219,4 @@ def print_interaction(interaction: TestcaseInteraction):
219
219
  text.stylize('status')
220
220
  else:
221
221
  text.stylize('info')
222
- console.console.print(text, end='')
222
+ console.console.print(text)
@@ -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(uri='rsalesc/test-preset', env=pathlib.Path('env.rbx.yml'))
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: