rbx.cp 0.13.3__py3-none-any.whl → 0.13.5__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 (73) hide show
  1. rbx/annotations.py +5 -5
  2. rbx/box/checkers.py +26 -22
  3. rbx/box/cli.py +0 -4
  4. rbx/box/code.py +27 -80
  5. rbx/box/contest/build_contest_statements.py +16 -3
  6. rbx/box/contest/schema.py +1 -2
  7. rbx/box/environment.py +16 -6
  8. rbx/box/fields.py +25 -1
  9. rbx/box/generators.py +31 -5
  10. rbx/box/global_package.py +6 -2
  11. rbx/box/header.py +31 -11
  12. rbx/box/package.py +3 -15
  13. rbx/box/presets/__init__.py +2 -2
  14. rbx/box/schema.py +4 -25
  15. rbx/box/setter_config.py +11 -0
  16. rbx/box/solutions.py +12 -4
  17. rbx/box/statements/build_statements.py +5 -1
  18. rbx/box/statements/builders.py +7 -7
  19. rbx/box/statements/schema.py +11 -2
  20. rbx/box/tasks.py +9 -4
  21. rbx/box/testcase_utils.py +2 -0
  22. rbx/box/testing/__init__.py +0 -0
  23. rbx/box/testing/testing_package.py +246 -0
  24. rbx/box/testing/testing_preset.py +36 -0
  25. rbx/box/testing/testing_shared.py +81 -0
  26. rbx/box/ui/screens/run_explorer.py +0 -8
  27. rbx/box/ui/utils/run_ui.py +7 -3
  28. rbx/box/ui/widgets/test_output_box.py +1 -1
  29. rbx/box/validators.py +5 -2
  30. rbx/grading/caching.py +67 -16
  31. rbx/grading/judge/program.py +268 -0
  32. rbx/grading/judge/sandbox.py +30 -193
  33. rbx/grading/judge/sandboxes/stupid_sandbox.py +232 -241
  34. rbx/grading/judge/sandboxes/tee.py +31 -0
  35. rbx/grading/steps.py +87 -199
  36. rbx/grading/steps_with_caching.py +15 -6
  37. rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
  38. rbx/resources/presets/default/shared/contest_template.rbx.tex +1 -1
  39. rbx/resources/presets/default/shared/problem_template.rbx.tex +5 -1
  40. rbx/resources/templates/rbx.h +43 -2
  41. rbx/testing_utils.py +8 -1
  42. rbx/utils.py +59 -1
  43. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/METADATA +2 -1
  44. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/RECORD +47 -67
  45. rbx/box/conftest.py +0 -42
  46. rbx/box/generators_test.py +0 -67
  47. rbx/box/lazy_importing_test.py +0 -25
  48. rbx/box/solutions_test.py +0 -47
  49. rbx/box/validators_test.py +0 -15
  50. rbx/checker.py +0 -128
  51. rbx/clone.py +0 -197
  52. rbx/conftest.py +0 -38
  53. rbx/create.py +0 -37
  54. rbx/edit.py +0 -24
  55. rbx/grading/conftest.py +0 -33
  56. rbx/grading/judge/sandboxes/isolate.py +0 -695
  57. rbx/grading/judge/testiso.py +0 -54
  58. rbx/grading/steps_with_caching_run_test.py +0 -707
  59. rbx/grading_utils.py +0 -148
  60. rbx/hydration.py +0 -101
  61. rbx/main.py +0 -118
  62. rbx/metadata.py +0 -105
  63. rbx/resources/envs/isolate.rbx.yml +0 -36
  64. rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
  65. rbx/run.py +0 -45
  66. rbx/schema.py +0 -64
  67. rbx/submit.py +0 -61
  68. rbx/test.py +0 -349
  69. rbx/testcase.py +0 -70
  70. rbx/testcase_rendering.py +0 -79
  71. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/LICENSE +0 -0
  72. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/WHEEL +0 -0
  73. {rbx_cp-0.13.3.dist-info → rbx_cp-0.13.5.dist-info}/entry_points.txt +0 -0
rbx/box/header.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import functools
2
2
  import importlib
3
3
  import importlib.resources
4
+ import json
4
5
  import pathlib
5
6
  from typing import Callable, Dict, Type
6
7
 
7
8
  from rbx.box import package
8
- from rbx.box.schema import Primitive
9
+ from rbx.box.fields import Primitive
9
10
 
10
11
 
11
12
  @functools.cache
@@ -40,12 +41,33 @@ def _preprocess_header(header: str) -> str:
40
41
  )
41
42
 
42
43
 
44
+ def _string_repr(s):
45
+ return json.dumps(s)
46
+
47
+
43
48
  def _get_string_var_block() -> str:
44
- return _get_var_block(_get_vars_of_type(str, lambda x: f'{x:!r}'))
49
+ return _get_var_block(_get_vars_of_type(str, _string_repr))
50
+
51
+
52
+ def _check_int_bounds(x: int) -> None:
53
+ if x >= 2**64:
54
+ raise ValueError(
55
+ f'Some variable you defined (value: {x}) is too large to fit in a C++ 64-bit integer (signed or unsigned)'
56
+ )
57
+ if x < -(2**63):
58
+ raise ValueError(
59
+ f'Some variable you defined (value: {x}) is too small to fit in a C++ 64-bit signed integer (int64_t)'
60
+ )
45
61
 
46
62
 
47
63
  def _get_int_var_block() -> str:
48
- return _get_var_block(_get_vars_of_type(int, lambda x: str(x)))
64
+ def _transform(x: Primitive) -> str:
65
+ if isinstance(x, bool):
66
+ return str(int(x))
67
+ _check_int_bounds(int(x))
68
+ return f'static_cast<int64_t>({x})'
69
+
70
+ return _get_var_block(_get_vars_of_type(int, _transform))
49
71
 
50
72
 
51
73
  def _get_float_var_block() -> str:
@@ -56,16 +78,14 @@ def _get_bool_var_block() -> str:
56
78
  return _get_var_block(_get_vars_of_type(bool, lambda x: 'true' if x else 'false'))
57
79
 
58
80
 
59
- def _get_vars_of_type(
60
- type: Type, transform: Callable[[Primitive], str]
61
- ) -> Dict[str, str]:
81
+ def _get_vars_of_type(t: Type, transform: Callable[[Primitive], str]) -> Dict[str, str]:
62
82
  pkg = package.find_problem_package_or_die()
63
83
  vars = pkg.expanded_vars
64
- return {
65
- name: transform(value)
66
- for name, value in vars.items()
67
- if isinstance(value, type)
68
- }
84
+
85
+ def is_valid(value: Primitive) -> bool:
86
+ return isinstance(value, t)
87
+
88
+ return {name: transform(value) for name, value in vars.items() if is_valid(value)}
69
89
 
70
90
 
71
91
  def _get_var_block(mappings: Dict[str, str]) -> str:
rbx/box/package.py CHANGED
@@ -1,8 +1,6 @@
1
1
  import atexit
2
2
  import functools
3
- import os
4
3
  import pathlib
5
- import shutil
6
4
  import sys
7
5
  from typing import Dict, List, Optional, Tuple
8
6
 
@@ -181,8 +179,10 @@ def get_file_cacher(root: pathlib.Path = pathlib.Path()) -> FileCacher:
181
179
 
182
180
  @functools.cache
183
181
  def get_digest_as_string(
184
- digest: str, root: pathlib.Path = pathlib.Path()
182
+ digest: Optional[str], root: pathlib.Path = pathlib.Path()
185
183
  ) -> Optional[str]:
184
+ if not digest:
185
+ return None
186
186
  cacher = get_file_cacher(root)
187
187
  try:
188
188
  content = cacher.get_file_content(digest)
@@ -453,18 +453,6 @@ def get_empty_sentinel_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path
453
453
  return path
454
454
 
455
455
 
456
- @functools.cache
457
- def get_fifos(root: pathlib.Path = pathlib.Path()) -> Tuple[pathlib.Path, pathlib.Path]:
458
- path = get_shared_dir(root) / '.fifos'
459
- shutil.rmtree(path, ignore_errors=True)
460
- path.mkdir(parents=True, exist_ok=True)
461
- fifo_in = path / 'fifo.in'
462
- fifo_out = path / 'fifo.out'
463
- os.mkfifo(fifo_in)
464
- os.mkfifo(fifo_out)
465
- return fifo_in, fifo_out
466
-
467
-
468
456
  @functools.cache
469
457
  def get_merged_capture_path(root: pathlib.Path = pathlib.Path()) -> pathlib.Path:
470
458
  path = get_shared_dir(root) / '.merged_capture'
@@ -339,9 +339,9 @@ def _copy_preset_file(
339
339
 
340
340
  # The symlink points somewhere inside the preset folder, fix the symlink.
341
341
  dst_absolute_path = utils.abspath(dst)
342
- fixed_target_relative_path = target_absolute_path.relative_to(
342
+ fixed_target_relative_path = utils.relpath(
343
+ target_absolute_path,
343
344
  dst_absolute_path.parent,
344
- walk_up=True,
345
345
  )
346
346
  dst.symlink_to(fixed_target_relative_path)
347
347
 
rbx/box/schema.py CHANGED
@@ -3,19 +3,17 @@ from __future__ import annotations
3
3
  import os
4
4
  import pathlib
5
5
  import re
6
- from typing import Annotated, Any, Dict, List, Optional, Union
6
+ from typing import Annotated, Any, Dict, List, Optional
7
7
 
8
8
  from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator
9
9
  from pydantic_core import PydanticCustomError
10
10
 
11
11
  from rbx.autoenum import AutoEnum, alias
12
- from rbx.box.fields import NameField
12
+ from rbx.box.fields import NameField, Primitive, expand_vars
13
13
  from rbx.box.statements.expander import expand_statements
14
14
  from rbx.box.statements.schema import Statement
15
15
  from rbx.grading.steps import Outcome
16
16
 
17
- Primitive = Union[str, int, float, bool]
18
-
19
17
 
20
18
  def _check_oneof(model_obj: BaseModel, fields: List[str]):
21
19
  has = []
@@ -30,27 +28,6 @@ def _check_oneof(model_obj: BaseModel, fields: List[str]):
30
28
  )
31
29
 
32
30
 
33
- def expand_var(value: Primitive) -> Primitive:
34
- if not isinstance(value, str):
35
- return value
36
- if value.startswith('\\'):
37
- return value[1:]
38
- if not value.startswith('py`') or not value.endswith('`'):
39
- return value
40
- res = eval(value[3:-1])
41
- for supported_type in [str, int, float, bool]:
42
- if isinstance(res, supported_type):
43
- return res
44
-
45
- raise TypeError(
46
- f'Variable with backticks should evaluate to a primitive Python type: {value}'
47
- )
48
-
49
-
50
- def expand_vars(vars: Dict[str, Primitive]) -> Dict[str, Primitive]:
51
- return {key: expand_var(value) for key, value in vars.items()}
52
-
53
-
54
31
  def _represents_int(s: str) -> bool:
55
32
  return re.match(r'[-+]?\d+$', s.strip()) is not None
56
33
 
@@ -245,6 +222,8 @@ Whether this interactor is a legacy interactor and needs a checker to be specifi
245
222
 
246
223
 
247
224
  class Testcase(BaseModel):
225
+ __test__ = False
226
+
248
227
  model_config = ConfigDict(extra='forbid')
249
228
 
250
229
  inputPath: pathlib.Path = Field(description="""The path of the input file.""")
rbx/box/setter_config.py CHANGED
@@ -66,6 +66,13 @@ class CachingConfig(BaseModel):
66
66
  )
67
67
 
68
68
 
69
+ class JudgingConfig(BaseModel):
70
+ check_stack: bool = Field(
71
+ default=True,
72
+ description='Whether to check the stack size before running code.',
73
+ )
74
+
75
+
69
76
  class SetterConfig(BaseModel):
70
77
  sanitizers: SanitizersConfig = Field(
71
78
  default_factory=SanitizersConfig, # type: ignore
@@ -93,6 +100,10 @@ class SetterConfig(BaseModel):
93
100
  default_factory=CachingConfig, # type: ignore
94
101
  description='Configuration for caching.',
95
102
  )
103
+ judging: JudgingConfig = Field(
104
+ default_factory=JudgingConfig, # type: ignore
105
+ description='Configuration for judging.',
106
+ )
96
107
 
97
108
  def substitute_command(self, command: str, sanitized: bool = False) -> str:
98
109
  exe = shlex.split(command)[0]
rbx/box/solutions.py CHANGED
@@ -922,6 +922,12 @@ def get_worst_outcome(evals: List[Evaluation]) -> Outcome:
922
922
  return Outcome.worst_outcome(eval.result.outcome for eval in evals)
923
923
 
924
924
 
925
+ def get_truncated_message(message: str, max_length: int = 100) -> str:
926
+ if len(message) > max_length:
927
+ return message[:max_length] + '... (truncated)'
928
+ return message
929
+
930
+
925
931
  class SolutionOutcomeReport(BaseModel):
926
932
  solution: Solution
927
933
  evals: List[Evaluation]
@@ -971,9 +977,8 @@ class SolutionOutcomeReport(BaseModel):
971
977
  if print_message and self.message is not None:
972
978
  tc, msg = self.message
973
979
  if msg:
974
- if len(msg) > 100:
975
- msg = msg[:100] + '... (truncated)'
976
- res += f'\nMessage for {tc}: {msg}'
980
+ msg = get_truncated_message(msg)
981
+ res += f'\nMessage for {utils.escape_markup(str(tc))}: {utils.escape_markup(msg)}'
977
982
  return res
978
983
 
979
984
 
@@ -1471,7 +1476,10 @@ async def print_run_report(
1471
1476
  console.print(f' ({time}, {memory})', end='')
1472
1477
  checker_msg = eval.result.message
1473
1478
  if checker_msg:
1474
- console.print(f': [i]{checker_msg}[/i]', end='')
1479
+ checker_msg = get_truncated_message(checker_msg, 150)
1480
+ console.print(
1481
+ f': [i]{utils.escape_markup(checker_msg)}[/i]', end=''
1482
+ )
1475
1483
  else:
1476
1484
  console.print(f'{i}/', end='')
1477
1485
  console.print(get_testcase_markup_verdict(eval), end='')
@@ -262,7 +262,6 @@ def build_statement_bytes(
262
262
  languages=get_environment_languages_for_statement(),
263
263
  params=params,
264
264
  root=pathlib.Path(td),
265
- custom_vars=custom_vars,
266
265
  ),
267
266
  item=StatementBuilderProblem(
268
267
  package=pkg,
@@ -271,6 +270,11 @@ def build_statement_bytes(
271
270
  get_samples() if use_samples else []
272
271
  ),
273
272
  short_name=short_name,
273
+ vars={
274
+ **pkg.expanded_vars,
275
+ **statement.expanded_vars,
276
+ **(custom_vars or {}),
277
+ },
274
278
  ),
275
279
  verbose=False,
276
280
  )
@@ -11,7 +11,8 @@ import typer
11
11
  from pydantic import BaseModel
12
12
 
13
13
  from rbx import console, utils
14
- from rbx.box.schema import Package, Primitive, Testcase
14
+ from rbx.box.fields import Primitive
15
+ from rbx.box.schema import Package, Testcase
15
16
  from rbx.box.statements.latex_jinja import (
16
17
  JinjaDictWrapper,
17
18
  render_latex_template,
@@ -48,8 +49,6 @@ class StatementBuilderContext:
48
49
  languages: List[StatementCodeLanguage]
49
50
  params: ConversionStep
50
51
  root: pathlib.Path
51
- custom_vars: Optional[Dict[str, Any]] = None
52
- vars: Optional[Dict[str, Primitive]] = None
53
52
 
54
53
  def build_jinja_kwargs(self) -> Dict[str, Any]:
55
54
  res = {
@@ -57,9 +56,6 @@ class StatementBuilderContext:
57
56
  'languages': self.languages,
58
57
  'keyed_languages': {lang.id: lang for lang in self.languages},
59
58
  }
60
- if self.vars is not None or self.custom_vars is not None:
61
- res['vars'] = self.vars or {}
62
- res['vars'].update(self.custom_vars or {})
63
59
  return res
64
60
 
65
61
 
@@ -125,12 +121,14 @@ class StatementBuilderProblem(StatementBuilderItem):
125
121
  # Will only be filled by contests.
126
122
  io_path: Optional[pathlib.Path] = None
127
123
 
124
+ vars: Optional[Dict[str, Primitive]] = None
125
+
128
126
  def build_inner_jinja_kwargs(self) -> Dict[str, Any]:
129
127
  kwargs = {
130
128
  'package': self.package,
131
129
  'statement': self.statement,
132
130
  'samples': self.samples,
133
- 'vars': JinjaDictWrapper(self.package.expanded_vars, key='vars'),
131
+ 'vars': JinjaDictWrapper(self.vars or {}, key='vars'),
134
132
  'title': self.statement.title or self.package.name,
135
133
  }
136
134
  if self.short_name is not None:
@@ -152,6 +150,7 @@ class StatementBuilderContest(StatementBuilderItem):
152
150
  location: Optional[str] = None
153
151
  date: Optional[str] = None
154
152
  problems: List[StatementBuilderProblem] = dataclasses.field(default_factory=list)
153
+ vars: Optional[Dict[str, Primitive]] = None
155
154
 
156
155
  def build_inner_jinja_kwargs(self) -> Dict[str, Any]:
157
156
  res = {'title': self.title}
@@ -167,6 +166,7 @@ class StatementBuilderContest(StatementBuilderItem):
167
166
  'problems': [
168
167
  problem.build_inner_jinja_kwargs() for problem in self.problems
169
168
  ],
169
+ 'vars': JinjaDictWrapper(self.vars or {}, key='vars'),
170
170
  }
171
171
  return res
172
172
 
@@ -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, List, Literal, Optional, Union
5
+ from typing import Annotated, Dict, 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, Primitive, expand_var
11
11
  from rbx.box.lang import is_valid_lang_code
12
12
 
13
13
 
@@ -175,3 +175,12 @@ the statement. Files will be included in the same folder as the statement file,
175
175
  their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
176
176
  """,
177
177
  )
178
+
179
+ vars: Dict[str, Primitive] = Field(
180
+ default={},
181
+ description='Variables to be used in the statement.',
182
+ )
183
+
184
+ @property
185
+ def expanded_vars(self) -> Dict[str, Primitive]:
186
+ return {key: expand_var(value) for key, value in self.vars.items()}
rbx/box/tasks.py CHANGED
@@ -51,7 +51,7 @@ async def run_solution_on_testcase(
51
51
  timelimit_override: Optional[int] = None,
52
52
  use_retries: bool = True,
53
53
  use_timelimit: bool = True,
54
- capture_pipes: bool = False,
54
+ capture_pipes: Optional[bool] = None,
55
55
  nruns: int = 0,
56
56
  filestem: Optional[str] = None,
57
57
  is_stress: bool = False,
@@ -175,12 +175,13 @@ async def _run_communication_solution_on_testcase(
175
175
  timelimit_override: Optional[int] = None,
176
176
  use_retries: bool = True,
177
177
  use_timelimit: bool = True,
178
- capture_pipes: bool = False,
178
+ capture_pipes: Optional[bool] = None,
179
179
  nruns: int = 0,
180
180
  filestem: Optional[str] = None,
181
181
  is_stress: bool = False,
182
182
  ) -> Evaluation:
183
- capture_pipes = capture_pipes or state.STATE.debug_logs
183
+ if capture_pipes is None:
184
+ capture_pipes = state.STATE.debug_logs
184
185
 
185
186
  async def run_fn(retry_index: int) -> Evaluation:
186
187
  actual_sandbox = package.get_singleton_sandbox()
@@ -243,6 +244,7 @@ async def _run_communication_solution_on_testcase(
243
244
  capture=DigestOrDest.create(interactor_capture_path)
244
245
  if interactor_capture_path
245
246
  else None,
247
+ file_prefix='interactor',
246
248
  )
247
249
  solution_capture_path = (
248
250
  output_path.with_suffix('.pout') if capture_pipes else None
@@ -255,6 +257,7 @@ async def _run_communication_solution_on_testcase(
255
257
  capture=DigestOrDest.create(solution_capture_path)
256
258
  if solution_capture_path
257
259
  else None,
260
+ file_prefix='solution',
258
261
  )
259
262
 
260
263
  merged_capture_path = output_path.with_suffix('.pio') if capture_pipes else None
@@ -262,7 +265,9 @@ async def _run_communication_solution_on_testcase(
262
265
  interactor=interactor_item,
263
266
  solution=solution_item,
264
267
  retry_index=retry_index,
265
- merged_capture=merged_capture_path,
268
+ merged_capture=DigestOrDest.create(merged_capture_path)
269
+ if merged_capture_path
270
+ else None,
266
271
  )
267
272
 
268
273
  checker_result = await checkers.check_communication(
rbx/box/testcase_utils.py CHANGED
@@ -14,6 +14,8 @@ from rbx.box.schema import Testcase, TestcaseGroup
14
14
 
15
15
 
16
16
  class TestcaseEntry(BaseModel):
17
+ __test__ = False
18
+
17
19
  group: str
18
20
  index: int
19
21
 
File without changes
@@ -0,0 +1,246 @@
1
+ import pathlib
2
+ from dataclasses import dataclass
3
+ from typing import Dict, List, Optional
4
+
5
+ from rbx import console, utils
6
+ from rbx.box import presets
7
+ from rbx.box.fields import Primitive
8
+ from rbx.box.schema import (
9
+ CodeItem,
10
+ ExpectedOutcome,
11
+ Generator,
12
+ Interactor,
13
+ Package,
14
+ Solution,
15
+ TaskType,
16
+ TestcaseGroup,
17
+ )
18
+ from rbx.box.testing.testing_preset import TestingPreset
19
+ from rbx.box.testing.testing_shared import PathOrStr, TestingShared
20
+ from rbx.grading.steps import Evaluation
21
+ from rbx.testing_utils import print_directory_tree
22
+
23
+
24
+ @dataclass
25
+ class TestcaseArtifacts:
26
+ output: Optional[str] = None
27
+ log: Optional[Evaluation] = None
28
+ interactor_input: Optional[str] = None
29
+ interactor_output: Optional[str] = None
30
+ interactor_pipes: Optional[str] = None
31
+
32
+
33
+ class TestingPackage(TestingShared):
34
+ def __init__(self, root: PathOrStr):
35
+ super().__init__(root)
36
+ self._yml = None
37
+
38
+ self.initialize()
39
+ self.preset = self.initialize_preset()
40
+
41
+ @property
42
+ def yml_path(self) -> pathlib.Path:
43
+ return self.root / 'problem.rbx.yml'
44
+
45
+ def initialize(self):
46
+ if not self.yml_path.exists():
47
+ self.yml_path.parent.mkdir(parents=True, exist_ok=True)
48
+ self.yml_path.touch()
49
+ self.yml_path.write_text(
50
+ utils.model_to_yaml(
51
+ Package(name='test-problem', timeLimit=1000, memoryLimit=256)
52
+ )
53
+ )
54
+
55
+ def initialize_preset(self) -> TestingPreset:
56
+ preset = presets.get_active_preset_or_null(self.root)
57
+ if preset is None:
58
+ preset_path = self.root / '.local.rbx'
59
+ preset_path.mkdir(parents=True, exist_ok=True)
60
+ else:
61
+ preset_path = presets.get_active_preset_path(self.root)
62
+ return TestingPreset(preset_path)
63
+
64
+ def print_tree(self):
65
+ print_directory_tree(self.root)
66
+
67
+ def print_yml(self):
68
+ console.console.print(self.yml_path.read_text(), highlight=True)
69
+
70
+ def print_debug(self):
71
+ self.print_yml()
72
+ self.print_tree()
73
+
74
+ @property
75
+ def yml(self) -> Package:
76
+ if self._yml is None:
77
+ self._yml = utils.model_from_yaml(Package, self.yml_path.read_text())
78
+ return self._yml
79
+
80
+ def save(self):
81
+ self.yml_path.write_text(utils.model_to_yaml(self.yml))
82
+
83
+ def set_type(self, type: TaskType):
84
+ self.yml.type = type
85
+ self.save()
86
+
87
+ def add_solution(
88
+ self,
89
+ path: PathOrStr,
90
+ outcome: ExpectedOutcome,
91
+ language: Optional[str] = None,
92
+ ):
93
+ self.yml.solutions = self.yml.solutions + [
94
+ Solution(path=pathlib.Path(path), language=language, outcome=outcome)
95
+ ]
96
+ self.save()
97
+ return self.add_file(path)
98
+
99
+ def add_generator(
100
+ self,
101
+ path: PathOrStr,
102
+ language: Optional[str] = None,
103
+ alias: Optional[str] = None,
104
+ src: Optional[PathOrStr] = None,
105
+ ):
106
+ if alias is not None:
107
+ self.yml.generators = self.yml.generators + [
108
+ Generator(path=pathlib.Path(path), language=language, name=alias)
109
+ ]
110
+ self.save()
111
+ return self.add_file(path, src=src)
112
+
113
+ def set_validator(
114
+ self,
115
+ path: PathOrStr,
116
+ language: Optional[str] = None,
117
+ src: Optional[PathOrStr] = None,
118
+ ):
119
+ self.yml.validator = CodeItem(path=pathlib.Path(path), language=language)
120
+ self.save()
121
+ return self.add_file(path, src=src)
122
+
123
+ def set_checker(
124
+ self,
125
+ path: PathOrStr,
126
+ language: Optional[str] = None,
127
+ src: Optional[PathOrStr] = None,
128
+ ):
129
+ self.yml.checker = CodeItem(path=pathlib.Path(path), language=language)
130
+ self.save()
131
+ return self.add_file(path, src=src)
132
+
133
+ def set_interactor(
134
+ self,
135
+ path: PathOrStr,
136
+ language: Optional[str] = None,
137
+ src: Optional[PathOrStr] = None,
138
+ ):
139
+ self.yml.interactor = Interactor(path=pathlib.Path(path), language=language)
140
+ self.save()
141
+ return self.add_file(path, src=src)
142
+
143
+ def set_var(self, name: str, value: Primitive):
144
+ self.yml.vars[name] = value
145
+ self.save()
146
+
147
+ def set_vars(self, vars: Dict[str, Primitive]):
148
+ self.yml.vars = vars
149
+ self.save()
150
+
151
+ def add_testplan(self, name: str, src: Optional[PathOrStr] = None):
152
+ path = self.add_file(pathlib.Path('testplan') / f'{name}.txt', src)
153
+ return path
154
+
155
+ def add_testscript(self, name: str, src: Optional[PathOrStr] = None):
156
+ path = self.add_file(pathlib.Path('testplan') / f'{name}.py', src)
157
+ return path
158
+
159
+ def add_testgroup_from_glob(
160
+ self,
161
+ name: str,
162
+ glob: str,
163
+ validator: Optional[PathOrStr] = None,
164
+ extra_validators: Optional[List[PathOrStr]] = None,
165
+ ):
166
+ self.yml.testcases = self.yml.testcases + [
167
+ TestcaseGroup(
168
+ name=name,
169
+ testcaseGlob=glob,
170
+ validator=CodeItem(path=pathlib.Path(validator)) if validator else None,
171
+ extraValidators=[
172
+ CodeItem(path=pathlib.Path(v)) for v in extra_validators
173
+ ]
174
+ if extra_validators
175
+ else [],
176
+ )
177
+ ]
178
+ self.save()
179
+
180
+ def add_testgroup_from_plan(
181
+ self,
182
+ name: str,
183
+ plan: str,
184
+ validator: Optional[PathOrStr] = None,
185
+ extra_validators: Optional[List[PathOrStr]] = None,
186
+ ):
187
+ plan_path = self.add_testplan(name)
188
+ plan_path.write_text(plan)
189
+ self.yml.testcases = self.yml.testcases + [
190
+ TestcaseGroup(
191
+ name=name,
192
+ generatorScript=CodeItem(path=plan_path),
193
+ validator=CodeItem(path=pathlib.Path(validator)) if validator else None,
194
+ extraValidators=[
195
+ CodeItem(path=pathlib.Path(v)) for v in extra_validators
196
+ ]
197
+ if extra_validators
198
+ else [],
199
+ )
200
+ ]
201
+ self.save()
202
+
203
+ def add_testgroup_from_script(
204
+ self,
205
+ name: str,
206
+ script: str,
207
+ validator: Optional[PathOrStr] = None,
208
+ extra_validators: Optional[List[PathOrStr]] = None,
209
+ ):
210
+ script_path = self.add_testscript(name)
211
+ script_path.write_text(script)
212
+ self.yml.testcases = self.yml.testcases + [
213
+ TestcaseGroup(
214
+ name=name,
215
+ generatorScript=CodeItem(path=script_path),
216
+ validator=CodeItem(path=pathlib.Path(validator)) if validator else None,
217
+ extraValidators=[
218
+ CodeItem(path=pathlib.Path(v)) for v in extra_validators
219
+ ]
220
+ if extra_validators
221
+ else [],
222
+ )
223
+ ]
224
+ self.save()
225
+
226
+ def get_build_testgroup_path(self, name: str) -> pathlib.Path:
227
+ return self.root / 'build' / 'tests' / name
228
+
229
+ def get_testcase_contents(self, path: pathlib.Path) -> TestcaseArtifacts:
230
+ contents = TestcaseArtifacts()
231
+ output_path = path.with_suffix('.out')
232
+ if output_path.exists():
233
+ contents.output = output_path.read_text()
234
+ log_path = path.with_suffix('.log')
235
+ if log_path.exists():
236
+ contents.log = Evaluation.model_validate_json(log_path.read_text())
237
+ interactor_input_path = path.with_suffix('.pin')
238
+ if interactor_input_path.exists():
239
+ contents.interactor_input = interactor_input_path.read_text()
240
+ interactor_output_path = path.with_suffix('.pout')
241
+ if interactor_output_path.exists():
242
+ contents.interactor_output = interactor_output_path.read_text()
243
+ interactor_pipes_path = path.with_suffix('.pio')
244
+ if interactor_pipes_path.exists():
245
+ contents.interactor_pipes = interactor_pipes_path.read_text()
246
+ return contents