rbx.cp 0.13.8__py3-none-any.whl → 0.16.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 (77) hide show
  1. rbx/__version__.py +1 -0
  2. rbx/box/cli.py +74 -70
  3. rbx/box/code.py +3 -0
  4. rbx/box/contest/build_contest_statements.py +65 -23
  5. rbx/box/contest/contest_package.py +8 -1
  6. rbx/box/contest/main.py +9 -3
  7. rbx/box/contest/schema.py +17 -13
  8. rbx/box/contest/statements.py +12 -8
  9. rbx/box/dump_schemas.py +2 -1
  10. rbx/box/environment.py +1 -1
  11. rbx/box/fields.py +22 -4
  12. rbx/box/generators.py +32 -13
  13. rbx/box/git_utils.py +29 -1
  14. rbx/box/limits_info.py +161 -0
  15. rbx/box/package.py +18 -1
  16. rbx/box/packaging/boca/boca_language_utils.py +26 -0
  17. rbx/box/packaging/boca/boca_outcome_utils.py +10 -0
  18. rbx/box/packaging/boca/packager.py +7 -5
  19. rbx/box/packaging/contest_main.py +20 -12
  20. rbx/box/packaging/packager.py +24 -14
  21. rbx/box/packaging/polygon/packager.py +7 -3
  22. rbx/box/packaging/polygon/upload.py +2 -1
  23. rbx/box/presets/__init__.py +143 -78
  24. rbx/box/presets/fetch.py +10 -2
  25. rbx/box/presets/schema.py +16 -1
  26. rbx/box/remote.py +3 -3
  27. rbx/box/sanitizers/issue_stack.py +124 -0
  28. rbx/box/schema.py +87 -27
  29. rbx/box/solutions.py +74 -117
  30. rbx/box/statements/build_statements.py +12 -1
  31. rbx/box/statements/builders.py +5 -3
  32. rbx/box/statements/latex_jinja.py +73 -23
  33. rbx/box/statements/schema.py +7 -9
  34. rbx/box/stressing/generator_parser.py +3 -1
  35. rbx/box/tasks.py +10 -10
  36. rbx/box/testcase_extractors.py +8 -0
  37. rbx/box/testing/testing_preset.py +129 -2
  38. rbx/box/testing/testing_shared.py +3 -1
  39. rbx/box/timing.py +305 -0
  40. rbx/box/tooling/boca/debug_utils.py +88 -0
  41. rbx/box/tooling/boca/manual_scrape.py +20 -0
  42. rbx/box/tooling/boca/scraper.py +660 -57
  43. rbx/box/unit.py +0 -2
  44. rbx/box/validators.py +0 -4
  45. rbx/grading/judge/cacher.py +36 -0
  46. rbx/grading/judge/program.py +12 -2
  47. rbx/grading/judge/sandbox.py +1 -1
  48. rbx/grading/judge/sandboxes/stupid_sandbox.py +2 -1
  49. rbx/grading/judge/storage.py +36 -3
  50. rbx/grading/limits.py +4 -0
  51. rbx/grading/steps.py +3 -2
  52. rbx/resources/presets/default/contest/contest.rbx.yml +7 -1
  53. rbx/resources/presets/default/contest/statement/info.rbx.tex +46 -0
  54. rbx/resources/presets/default/preset.rbx.yml +1 -0
  55. rbx/resources/presets/default/problem/.gitignore +1 -0
  56. rbx/resources/presets/default/problem/problem.rbx.yml +19 -3
  57. rbx/resources/presets/default/problem/rbx.h +52 -5
  58. rbx/resources/presets/default/problem/statement/statement.rbx.tex +6 -2
  59. rbx/resources/presets/default/problem/testlib.h +6299 -0
  60. rbx/resources/presets/default/problem/validator.cpp +4 -3
  61. rbx/resources/presets/default/shared/contest_template.rbx.tex +8 -4
  62. rbx/resources/presets/default/shared/icpc.sty +18 -3
  63. rbx/resources/presets/default/shared/problem_template.rbx.tex +4 -1
  64. rbx/testing_utils.py +17 -1
  65. rbx/utils.py +45 -0
  66. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/METADATA +5 -2
  67. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/RECORD +71 -67
  68. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/entry_points.txt +0 -1
  69. rbx/providers/__init__.py +0 -43
  70. rbx/providers/codeforces.py +0 -73
  71. rbx/providers/provider.py +0 -26
  72. rbx/submitors/__init__.py +0 -18
  73. rbx/submitors/codeforces.py +0 -121
  74. rbx/submitors/submitor.py +0 -25
  75. /rbx/resources/presets/default/problem/sols/{wa.cpp → wa-overflow.cpp} +0 -0
  76. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/LICENSE +0 -0
  77. {rbx_cp-0.13.8.dist-info → rbx_cp-0.16.0.dist-info}/WHEEL +0 -0
@@ -7,7 +7,7 @@ import syncer
7
7
  import typer
8
8
 
9
9
  from rbx import annotations, console, utils
10
- from rbx.box import environment, naming, package
10
+ from rbx.box import environment, limits_info, naming, package
11
11
  from rbx.box.formatting import href
12
12
  from rbx.box.schema import Package, expand_any_vars
13
13
  from rbx.box.statements.builders import (
@@ -264,6 +264,9 @@ def build_statement_bytes(
264
264
  root=pathlib.Path(td),
265
265
  ),
266
266
  item=StatementBuilderProblem(
267
+ limits=limits_info.get_limits_profile(
268
+ profile=limits_info.get_active_profile()
269
+ ),
267
270
  package=pkg,
268
271
  statement=statement,
269
272
  samples=StatementSample.from_testcases(
@@ -302,6 +305,14 @@ def build_statement(
302
305
  statement_path = (package.get_build_path() / statement.name).with_suffix(
303
306
  last_output.get_file_suffix()
304
307
  )
308
+ active_profile = limits_info.get_active_profile()
309
+ if (
310
+ active_profile is not None
311
+ and limits_info.get_saved_limits_profile(active_profile) is not None
312
+ ):
313
+ statement_path = statement_path.with_stem(
314
+ f'{statement_path.stem}-{active_profile}'
315
+ )
305
316
  statement_path.parent.mkdir(parents=True, exist_ok=True)
306
317
  statement_path.write_bytes(last_content)
307
318
  console.console.print(
@@ -12,7 +12,7 @@ from pydantic import BaseModel
12
12
 
13
13
  from rbx import console, utils
14
14
  from rbx.box.fields import Primitive
15
- from rbx.box.schema import Package, Testcase
15
+ from rbx.box.schema import LimitsProfile, Package, Testcase
16
16
  from rbx.box.statements.latex_jinja import (
17
17
  JinjaDictWrapper,
18
18
  render_latex_template,
@@ -115,6 +115,7 @@ class ExplainedStatementSample(StatementSample):
115
115
  class StatementBuilderProblem(StatementBuilderItem):
116
116
  package: Package
117
117
  statement: Statement
118
+ limits: LimitsProfile
118
119
  samples: List[StatementSample] = dataclasses.field(default_factory=list)
119
120
  short_name: Optional[str] = None
120
121
 
@@ -128,8 +129,9 @@ class StatementBuilderProblem(StatementBuilderItem):
128
129
  'package': self.package,
129
130
  'statement': self.statement,
130
131
  'samples': self.samples,
131
- 'vars': JinjaDictWrapper(self.vars or {}, key='vars'),
132
+ 'vars': JinjaDictWrapper.from_dict(self.vars or {}, wrapper_key='vars'),
132
133
  'title': self.statement.title or self.package.name,
134
+ 'limits': self.limits,
133
135
  }
134
136
  if self.short_name is not None:
135
137
  kwargs['short_name'] = self.short_name
@@ -166,7 +168,7 @@ class StatementBuilderContest(StatementBuilderItem):
166
168
  'problems': [
167
169
  problem.build_inner_jinja_kwargs() for problem in self.problems
168
170
  ],
169
- 'vars': JinjaDictWrapper(self.vars or {}, key='vars'),
171
+ 'vars': JinjaDictWrapper.from_dict(self.vars or {}, wrapper_key='vars'),
170
172
  }
171
173
  return res
172
174
 
@@ -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
 
@@ -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: