rbx.cp 0.5.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/__init__.py +0 -0
- rbx/annotations.py +127 -0
- rbx/autoenum.py +333 -0
- rbx/box/__init__.py +0 -0
- rbx/box/builder.py +77 -0
- rbx/box/cd.py +37 -0
- rbx/box/checkers.py +134 -0
- rbx/box/code.py +185 -0
- rbx/box/compile.py +56 -0
- rbx/box/conftest.py +42 -0
- rbx/box/contest/__init__.py +0 -0
- rbx/box/contest/build_contest_statements.py +347 -0
- rbx/box/contest/contest_package.py +76 -0
- rbx/box/contest/contest_utils.py +20 -0
- rbx/box/contest/main.py +179 -0
- rbx/box/contest/schema.py +155 -0
- rbx/box/contest/statements.py +82 -0
- rbx/box/creation.py +72 -0
- rbx/box/download.py +64 -0
- rbx/box/environment.py +345 -0
- rbx/box/extensions.py +26 -0
- rbx/box/generators.py +478 -0
- rbx/box/generators_test.py +63 -0
- rbx/box/main.py +449 -0
- rbx/box/package.py +316 -0
- rbx/box/packaging/boca/extension.py +27 -0
- rbx/box/packaging/boca/packager.py +245 -0
- rbx/box/packaging/contest_main.py +82 -0
- rbx/box/packaging/main.py +68 -0
- rbx/box/packaging/packager.py +117 -0
- rbx/box/packaging/polygon/packager.py +320 -0
- rbx/box/packaging/polygon/test.py +81 -0
- rbx/box/packaging/polygon/xml_schema.py +106 -0
- rbx/box/presets/__init__.py +503 -0
- rbx/box/presets/fetch.py +70 -0
- rbx/box/presets/lock_schema.py +20 -0
- rbx/box/presets/schema.py +59 -0
- rbx/box/schema.py +394 -0
- rbx/box/solutions.py +792 -0
- rbx/box/solutions_test.py +41 -0
- rbx/box/statements/__init__.py +0 -0
- rbx/box/statements/build_statements.py +359 -0
- rbx/box/statements/builders.py +375 -0
- rbx/box/statements/joiners.py +113 -0
- rbx/box/statements/latex.py +47 -0
- rbx/box/statements/latex_jinja.py +214 -0
- rbx/box/statements/schema.py +138 -0
- rbx/box/stresses.py +292 -0
- rbx/box/stressing/__init__.py +0 -0
- rbx/box/stressing/finder_parser.py +359 -0
- rbx/box/stressing/generator_parser.py +258 -0
- rbx/box/testcases.py +54 -0
- rbx/box/ui/__init__.py +0 -0
- rbx/box/ui/captured_log.py +372 -0
- rbx/box/ui/css/app.tcss +48 -0
- rbx/box/ui/main.py +38 -0
- rbx/box/ui/run.py +209 -0
- rbx/box/validators.py +245 -0
- rbx/box/validators_test.py +15 -0
- rbx/checker.py +128 -0
- rbx/clone.py +197 -0
- rbx/config.py +271 -0
- rbx/conftest.py +38 -0
- rbx/console.py +27 -0
- rbx/create.py +37 -0
- rbx/edit.py +24 -0
- rbx/grading/__init__.py +0 -0
- rbx/grading/caching.py +356 -0
- rbx/grading/conftest.py +33 -0
- rbx/grading/judge/__init__.py +0 -0
- rbx/grading/judge/cacher.py +503 -0
- rbx/grading/judge/digester.py +35 -0
- rbx/grading/judge/sandbox.py +748 -0
- rbx/grading/judge/sandboxes/__init__.py +0 -0
- rbx/grading/judge/sandboxes/isolate.py +683 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
- rbx/grading/judge/sandboxes/timeit.py +217 -0
- rbx/grading/judge/storage.py +284 -0
- rbx/grading/judge/test.py +38 -0
- rbx/grading/judge/testiso.py +54 -0
- rbx/grading/steps.py +522 -0
- rbx/grading/steps_with_caching.py +59 -0
- rbx/grading/steps_with_caching_run_test.py +429 -0
- rbx/grading_utils.py +148 -0
- rbx/hydration.py +101 -0
- rbx/main.py +122 -0
- rbx/metadata.py +105 -0
- rbx/providers/__init__.py +43 -0
- rbx/providers/codeforces.py +73 -0
- rbx/providers/provider.py +26 -0
- rbx/resources/checkers/boilerplate.cpp +20 -0
- rbx/resources/default_config.json +48 -0
- rbx/resources/envs/default.rbx.yml +37 -0
- rbx/resources/envs/isolate.rbx.yml +37 -0
- rbx/resources/packagers/boca/checker.sh +43 -0
- rbx/resources/packagers/boca/compare +53 -0
- rbx/resources/packagers/boca/compile/c +172 -0
- rbx/resources/packagers/boca/compile/cc +173 -0
- rbx/resources/packagers/boca/compile/cpp +172 -0
- rbx/resources/packagers/boca/compile/java +194 -0
- rbx/resources/packagers/boca/compile/kt +155 -0
- rbx/resources/packagers/boca/compile/pas +172 -0
- rbx/resources/packagers/boca/compile/py2 +173 -0
- rbx/resources/packagers/boca/compile/py3 +173 -0
- rbx/resources/packagers/boca/run/c +128 -0
- rbx/resources/packagers/boca/run/cc +128 -0
- rbx/resources/packagers/boca/run/cpp +128 -0
- rbx/resources/packagers/boca/run/java +194 -0
- rbx/resources/packagers/boca/run/kt +159 -0
- rbx/resources/packagers/boca/run/py2 +166 -0
- rbx/resources/packagers/boca/run/py3 +166 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
- rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
- rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
- rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
- rbx/resources/presets/default/preset.rbx.yml +12 -0
- rbx/resources/presets/default/problem/.gitignore +6 -0
- rbx/resources/presets/default/problem/gen.cpp +9 -0
- rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
- rbx/resources/presets/default/problem/random.py +3 -0
- rbx/resources/presets/default/problem/random.txt +2 -0
- rbx/resources/presets/default/problem/sols/main.cpp +9 -0
- rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
- rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
- rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
- rbx/resources/presets/default/problem/statement/projecao.png +0 -0
- rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
- rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
- rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
- rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
- rbx/resources/presets/default/problem/validator.cpp +16 -0
- rbx/resources/presets/default/problem/wcmp.cpp +34 -0
- rbx/resources/templates/template.cpp +19 -0
- rbx/run.py +45 -0
- rbx/schema.py +64 -0
- rbx/submit.py +61 -0
- rbx/submitors/__init__.py +18 -0
- rbx/submitors/codeforces.py +120 -0
- rbx/submitors/submitor.py +25 -0
- rbx/test.py +347 -0
- rbx/testcase.py +70 -0
- rbx/testcase_rendering.py +79 -0
- rbx/testdata/box1/gen1.cpp +7 -0
- rbx/testdata/box1/gen2.cpp +9 -0
- rbx/testdata/box1/genScript.py +2 -0
- rbx/testdata/box1/hard-tle.sol.cpp +26 -0
- rbx/testdata/box1/ole.cpp +17 -0
- rbx/testdata/box1/problem.rbx.yml +39 -0
- rbx/testdata/box1/re.sol.cpp +23 -0
- rbx/testdata/box1/sol.cpp +22 -0
- rbx/testdata/box1/tests/1.in +1 -0
- rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
- rbx/testdata/box1/tle.sol.cpp +35 -0
- rbx/testdata/box1/validator.cpp +11 -0
- rbx/testdata/box1/wa.sol.cpp +22 -0
- rbx/testdata/caching/executable.py +1 -0
- rbx/testdata/compatible +0 -0
- rbx/testing_utils.py +65 -0
- rbx/utils.py +162 -0
- rbx_cp-0.5.0.dist-info/LICENSE +201 -0
- rbx_cp-0.5.0.dist-info/METADATA +89 -0
- rbx_cp-0.5.0.dist-info/RECORD +164 -0
- rbx_cp-0.5.0.dist-info/WHEEL +4 -0
- rbx_cp-0.5.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
"""This module provides a template-rendering function for Jinja2
|
2
|
+
that overrides Jinja2 defaults to make it work more seamlessly
|
3
|
+
with Latex.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import pathlib
|
7
|
+
import re
|
8
|
+
import typing
|
9
|
+
from typing import Dict, Tuple, Union
|
10
|
+
|
11
|
+
import jinja2
|
12
|
+
import typer
|
13
|
+
|
14
|
+
from rbx import console
|
15
|
+
|
16
|
+
######################################################################
|
17
|
+
# J2_ARGS
|
18
|
+
# Constant was borrowed from Marc Brinkmann's
|
19
|
+
# latex repository (mbr/latex on github)
|
20
|
+
######################################################################
|
21
|
+
J2_ARGS = {
|
22
|
+
'block_start_string': r'\BLOCK{',
|
23
|
+
'block_end_string': '}',
|
24
|
+
'variable_start_string': r'\VAR{',
|
25
|
+
'variable_end_string': '}',
|
26
|
+
'comment_start_string': r'\#{',
|
27
|
+
'comment_end_string': '}',
|
28
|
+
'line_statement_prefix': '%-',
|
29
|
+
'line_comment_prefix': '%#',
|
30
|
+
'trim_blocks': True,
|
31
|
+
'autoescape': False,
|
32
|
+
}
|
33
|
+
|
34
|
+
######################################################################
|
35
|
+
# Latex escape regex constants
|
36
|
+
######################################################################
|
37
|
+
|
38
|
+
# Organize all latex escape characters in one list
|
39
|
+
# (EXCEPT FOR ( "\" ), which is handled separately)
|
40
|
+
# escaping those which are special characters in
|
41
|
+
# PERL regular expressions
|
42
|
+
ESCAPE_CHARS = [
|
43
|
+
r'\&',
|
44
|
+
'%',
|
45
|
+
r'\$',
|
46
|
+
'#',
|
47
|
+
'_',
|
48
|
+
r'\{',
|
49
|
+
r'\}',
|
50
|
+
'~',
|
51
|
+
r'\^',
|
52
|
+
]
|
53
|
+
|
54
|
+
# For each latex escape character, create a regular expression
|
55
|
+
# that matches all of the following criteria
|
56
|
+
# 1) one or two characters
|
57
|
+
# 2) if two characters, the first character is NOT a backslash ( "\" )
|
58
|
+
# 3) if two characters, the second, if one, the first character
|
59
|
+
# is one of the latex escape characters
|
60
|
+
REGEX_ESCAPE_CHARS = [
|
61
|
+
(re.compile(r'(?<!\\)' + i), r'\\' + i.replace('\\', '')) for i in ESCAPE_CHARS
|
62
|
+
]
|
63
|
+
|
64
|
+
# Place escape characters in [] for "match any character" regex
|
65
|
+
ESCAPE_CHARS_OR = r'[{}\\]'.format(''.join(ESCAPE_CHARS))
|
66
|
+
|
67
|
+
# For the back slash, create a regular expression
|
68
|
+
# that matches all of the following criteria
|
69
|
+
# 1) one, two, or three characters
|
70
|
+
# 2) the first character is not a backslash
|
71
|
+
# 3) the second character is a backslash
|
72
|
+
# 4) the third character is none of the ESCAPE_CHARS,
|
73
|
+
# and is also not a backslash
|
74
|
+
REGEX_BACKSLASH = re.compile(r'(?<!\\)\\(?!{})'.format(ESCAPE_CHARS_OR))
|
75
|
+
|
76
|
+
|
77
|
+
######################################################################
|
78
|
+
# Declare module functions
|
79
|
+
######################################################################
|
80
|
+
def escape_latex_str_if_str(value):
|
81
|
+
"""Escape a latex string"""
|
82
|
+
if not isinstance(value, str):
|
83
|
+
return value
|
84
|
+
for regex, replace_text in REGEX_ESCAPE_CHARS:
|
85
|
+
value = re.sub(regex, replace_text, value)
|
86
|
+
value = re.sub(REGEX_BACKSLASH, r'\\textbackslash{}', value)
|
87
|
+
return value
|
88
|
+
|
89
|
+
|
90
|
+
def _process_zeroes(value: int) -> Tuple[int, int, int]:
|
91
|
+
cnt = 0
|
92
|
+
|
93
|
+
acc = value
|
94
|
+
while acc >= 10:
|
95
|
+
acc //= 10
|
96
|
+
cnt += 1
|
97
|
+
return acc, cnt, value - acc * 10**cnt
|
98
|
+
|
99
|
+
|
100
|
+
def scientific_notation(
|
101
|
+
value: Union[int, jinja2.Undefined], zeroes: int = 2
|
102
|
+
) -> Union[str, jinja2.Undefined]:
|
103
|
+
if jinja2.is_undefined(value):
|
104
|
+
return typing.cast(jinja2.Undefined, value)
|
105
|
+
assert isinstance(value, int)
|
106
|
+
assert zeroes >= 1
|
107
|
+
if value == 0:
|
108
|
+
return '0'
|
109
|
+
if value < 0:
|
110
|
+
return f'-{scientific_notation(-value, zeroes=zeroes)}'
|
111
|
+
|
112
|
+
mult, exp, rest = _process_zeroes(value)
|
113
|
+
if exp < zeroes:
|
114
|
+
return str(value)
|
115
|
+
res = '10' if exp == 1 else f'10^{exp}'
|
116
|
+
if rest > 0 and len(str(rest)) + 1 >= len(str(value)):
|
117
|
+
# Should not convert numbers like 532 to 5*10^2 + 32.
|
118
|
+
return str(value)
|
119
|
+
if mult > 1:
|
120
|
+
res = f'{mult} \\times {res}'
|
121
|
+
if rest > 0:
|
122
|
+
res = f'{res} + {rest}'
|
123
|
+
return res
|
124
|
+
|
125
|
+
|
126
|
+
def path_parent(path: pathlib.Path) -> pathlib.Path:
|
127
|
+
return path.parent
|
128
|
+
|
129
|
+
|
130
|
+
def path_stem(path: pathlib.Path) -> str:
|
131
|
+
return path.stem
|
132
|
+
|
133
|
+
|
134
|
+
######################################################################
|
135
|
+
# Declare module functions
|
136
|
+
######################################################################
|
137
|
+
|
138
|
+
|
139
|
+
class JinjaDictWrapper(dict):
|
140
|
+
def __init__(self, *args, key='dict object', **kwargs):
|
141
|
+
super().__init__(*args, **kwargs)
|
142
|
+
self.key = key
|
143
|
+
|
144
|
+
def __getitem__(self, key):
|
145
|
+
try:
|
146
|
+
return super().__getitem__(key)
|
147
|
+
except KeyError:
|
148
|
+
return jinja2.StrictUndefined(hint=f'"{key}" was not found in "{self.key}"')
|
149
|
+
|
150
|
+
|
151
|
+
def add_builtin_filters(j2_env: jinja2.Environment):
|
152
|
+
j2_env.filters['escape'] = escape_latex_str_if_str
|
153
|
+
j2_env.filters['sci'] = scientific_notation
|
154
|
+
j2_env.filters['parent'] = path_parent
|
155
|
+
j2_env.filters['stem'] = path_stem
|
156
|
+
|
157
|
+
|
158
|
+
def render_latex_template(path_templates, template_filename, template_vars=None) -> str:
|
159
|
+
"""Render a latex template, filling in its template variables
|
160
|
+
|
161
|
+
:param path_templates: the path to the template directory
|
162
|
+
:param template_filename: the name, rooted at the path_template_directory,
|
163
|
+
of the desired template for rendering
|
164
|
+
:param template_vars: dictionary of key:val for jinja2 variables
|
165
|
+
defaults to None for case when no values need to be passed
|
166
|
+
"""
|
167
|
+
var_dict = template_vars if template_vars else {}
|
168
|
+
j2_env = jinja2.Environment(
|
169
|
+
loader=jinja2.FileSystemLoader(path_templates),
|
170
|
+
**J2_ARGS,
|
171
|
+
undefined=jinja2.StrictUndefined,
|
172
|
+
)
|
173
|
+
add_builtin_filters(j2_env)
|
174
|
+
template = j2_env.get_template(template_filename)
|
175
|
+
try:
|
176
|
+
return template.render(**var_dict) # type: ignore
|
177
|
+
except jinja2.UndefinedError as err:
|
178
|
+
console.console.print('[error]Error while rendering Jinja2 template:', end=' ')
|
179
|
+
console.console.print(err)
|
180
|
+
console.console.print(
|
181
|
+
'[warning]This usually happens when accessing an undefined variable.[/warning]'
|
182
|
+
)
|
183
|
+
raise typer.Abort() from err
|
184
|
+
|
185
|
+
|
186
|
+
def render_latex_template_blocks(
|
187
|
+
path_templates, template_filename, template_vars=None
|
188
|
+
) -> Dict[str, str]:
|
189
|
+
"""Render a latex template, filling in its template variables
|
190
|
+
|
191
|
+
:param path_templates: the path to the template directory
|
192
|
+
:param template_filename: the name, rooted at the path_template_directory,
|
193
|
+
of the desired template for rendering
|
194
|
+
:param template_vars: dictionary of key:val for jinja2 variables
|
195
|
+
defaults to None for case when no values need to be passed
|
196
|
+
"""
|
197
|
+
var_dict = template_vars if template_vars else {}
|
198
|
+
j2_env = jinja2.Environment(
|
199
|
+
loader=jinja2.FileSystemLoader(path_templates),
|
200
|
+
**J2_ARGS,
|
201
|
+
undefined=jinja2.StrictUndefined,
|
202
|
+
)
|
203
|
+
add_builtin_filters(j2_env)
|
204
|
+
template = j2_env.get_template(template_filename)
|
205
|
+
ctx = template.new_context(var_dict) # type: ignore
|
206
|
+
try:
|
207
|
+
return {key: ''.join(value(ctx)) for key, value in template.blocks.items()}
|
208
|
+
except jinja2.UndefinedError as err:
|
209
|
+
console.console.print('[error]Error while rendering Jinja2 template:', end=' ')
|
210
|
+
console.console.print(err)
|
211
|
+
console.console.print(
|
212
|
+
'[warning]This usually happens when accessing an undefined variable.[/warning]'
|
213
|
+
)
|
214
|
+
raise typer.Abort() from err
|
@@ -0,0 +1,138 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import pathlib
|
4
|
+
from enum import Enum
|
5
|
+
from typing import List, Literal, Union
|
6
|
+
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
8
|
+
|
9
|
+
from rbx.autoenum import AutoEnum, alias
|
10
|
+
|
11
|
+
|
12
|
+
### Conversion types
|
13
|
+
class ConversionType(str, Enum):
|
14
|
+
rbxToTex = 'rbx-tex'
|
15
|
+
"""Conversion from rbxTeX to LaTeX."""
|
16
|
+
|
17
|
+
TexToPDF = 'tex2pdf'
|
18
|
+
"""Conversion from LaTeX to PDF using pdfLaTeX."""
|
19
|
+
|
20
|
+
JinjaTeX = 'jinja-tex'
|
21
|
+
"""Conversion from LaTeX with Jinja2 expressions to LaTeX."""
|
22
|
+
|
23
|
+
def __repr__(self):
|
24
|
+
return str.__repr__(self.value)
|
25
|
+
|
26
|
+
|
27
|
+
### Conversion nodes.
|
28
|
+
class rbxToTeX(BaseModel):
|
29
|
+
"""Configures the conversion between rbxTeX and LaTeX."""
|
30
|
+
|
31
|
+
type: Literal[ConversionType.rbxToTex]
|
32
|
+
|
33
|
+
template: pathlib.Path = Field(
|
34
|
+
default=pathlib.Path('template.rbx.tex'),
|
35
|
+
description='Path to the template that should be used to render the rbx-tex blocks.',
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
class TexToPDF(BaseModel):
|
40
|
+
"""Configures the conversion between LaTeX and PDF using pdfLaTeX."""
|
41
|
+
|
42
|
+
type: Literal[ConversionType.TexToPDF]
|
43
|
+
|
44
|
+
|
45
|
+
class JinjaTeX(BaseModel):
|
46
|
+
type: Literal[ConversionType.JinjaTeX]
|
47
|
+
|
48
|
+
|
49
|
+
### Joiner types.
|
50
|
+
class JoinerType(str, Enum):
|
51
|
+
TexToPDF = 'tex2pdf'
|
52
|
+
"""Join contest tex and problem texs to PDF using pdfLaTeX."""
|
53
|
+
|
54
|
+
def __repr__(self):
|
55
|
+
return str.__repr__(self.value)
|
56
|
+
|
57
|
+
|
58
|
+
### Joiner nodes.
|
59
|
+
class JoinTexToPDF(BaseModel):
|
60
|
+
"""Configures the joining of contest and problem texes to PDF."""
|
61
|
+
|
62
|
+
type: Literal[JoinerType.TexToPDF]
|
63
|
+
|
64
|
+
|
65
|
+
ConversionStep = Union[TexToPDF, JinjaTeX, rbxToTeX]
|
66
|
+
Joiner = JoinTexToPDF
|
67
|
+
|
68
|
+
|
69
|
+
### Statement types
|
70
|
+
class StatementType(AutoEnum):
|
71
|
+
rbxTeX = alias('rbx-tex', 'rbx-tex', 'rbx') # type: ignore
|
72
|
+
"""Statement written in rbxTeX format."""
|
73
|
+
|
74
|
+
TeX = alias('tex')
|
75
|
+
"""Statement written in pure LaTeX format."""
|
76
|
+
|
77
|
+
JinjaTeX = alias('jinja-tex')
|
78
|
+
"""Statement written in LaTeX format with Jinja2 expressions."""
|
79
|
+
|
80
|
+
PDF = alias('pdf')
|
81
|
+
"""Statement is a PDF."""
|
82
|
+
|
83
|
+
def get_file_suffix(self) -> str:
|
84
|
+
if self == StatementType.TeX:
|
85
|
+
return '.tex'
|
86
|
+
if self == StatementType.rbxTeX:
|
87
|
+
return '.rbx.tex'
|
88
|
+
if self == StatementType.JinjaTeX:
|
89
|
+
return '.jinja.tex'
|
90
|
+
if self == StatementType.PDF:
|
91
|
+
return '.pdf'
|
92
|
+
raise ValueError(f'Unknown statement type: {self}')
|
93
|
+
|
94
|
+
|
95
|
+
class Statement(BaseModel):
|
96
|
+
model_config = ConfigDict(extra='forbid')
|
97
|
+
|
98
|
+
title: str = Field(
|
99
|
+
description='Name of the problem, as it appears in the statement.'
|
100
|
+
)
|
101
|
+
|
102
|
+
path: pathlib.Path = Field(description='Path to the input statement file.')
|
103
|
+
|
104
|
+
type: StatementType = Field(description='Type of the input statement file.')
|
105
|
+
|
106
|
+
steps: List[ConversionStep] = Field(
|
107
|
+
[],
|
108
|
+
discriminator='type',
|
109
|
+
description="""
|
110
|
+
Describes a sequence of conversion steps that should be applied to the statement file.
|
111
|
+
|
112
|
+
Usually, it is not necessary to specify these, as they can be inferred from the
|
113
|
+
input statement type and the output statement type, but you can use this to force
|
114
|
+
certain conversion steps to happen.
|
115
|
+
""",
|
116
|
+
)
|
117
|
+
|
118
|
+
configure: List[ConversionStep] = Field(
|
119
|
+
[],
|
120
|
+
discriminator='type',
|
121
|
+
description="""
|
122
|
+
Configure how certain conversion steps should happen when applied to the statement file.
|
123
|
+
|
124
|
+
Different from the `steps` field, this does not force the steps to happen, but rather only
|
125
|
+
configure them in case they are applied.
|
126
|
+
""",
|
127
|
+
)
|
128
|
+
|
129
|
+
assets: List[str] = Field(
|
130
|
+
[],
|
131
|
+
description="""
|
132
|
+
Assets relative to the package directory that should be included while building
|
133
|
+
the statement. Files will be included in the same folder as the statement file, preserving
|
134
|
+
their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
|
135
|
+
""",
|
136
|
+
)
|
137
|
+
|
138
|
+
language: str = Field('en', description='Language this is statement is written in.')
|
rbx/box/stresses.py
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import functools
|
3
|
+
import time
|
4
|
+
from shutil import rmtree
|
5
|
+
from typing import List, Optional
|
6
|
+
|
7
|
+
import typer
|
8
|
+
from pydantic import BaseModel
|
9
|
+
|
10
|
+
from rbx import console
|
11
|
+
from rbx.box import checkers, package, validators
|
12
|
+
from rbx.box.code import compile_item, run_item
|
13
|
+
from rbx.box.generators import generate_standalone
|
14
|
+
from rbx.box.schema import CodeItem, GeneratorCall, Stress, Testcase
|
15
|
+
from rbx.box.solutions import compile_solutions, get_outcome_style_verdict
|
16
|
+
from rbx.box.stressing import finder_parser
|
17
|
+
from rbx.grading.steps import (
|
18
|
+
DigestOrDest,
|
19
|
+
DigestOrSource,
|
20
|
+
Outcome,
|
21
|
+
)
|
22
|
+
from rbx.utils import StatusProgress
|
23
|
+
|
24
|
+
|
25
|
+
class StressFinding(BaseModel):
|
26
|
+
generator: GeneratorCall
|
27
|
+
|
28
|
+
|
29
|
+
class StressReport(BaseModel):
|
30
|
+
findings: List[StressFinding] = []
|
31
|
+
executed: int = 0
|
32
|
+
|
33
|
+
|
34
|
+
def _compile_finder(finder: CodeItem) -> str:
|
35
|
+
try:
|
36
|
+
digest = compile_item(finder)
|
37
|
+
except Exception as e:
|
38
|
+
console.console.print(
|
39
|
+
f'[error]Failed compiling checker [item]{finder.path}[/item].[/error]'
|
40
|
+
)
|
41
|
+
raise typer.Exit(1) from e
|
42
|
+
return digest
|
43
|
+
|
44
|
+
|
45
|
+
def run_stress(
|
46
|
+
name: str,
|
47
|
+
timeoutInSeconds: int,
|
48
|
+
finder: Optional[str] = None,
|
49
|
+
args: Optional[str] = None,
|
50
|
+
findingsLimit: int = 1,
|
51
|
+
verbose: bool = False,
|
52
|
+
progress: Optional[StatusProgress] = None,
|
53
|
+
) -> StressReport:
|
54
|
+
if finder:
|
55
|
+
stress = Stress(
|
56
|
+
name=f'{name}',
|
57
|
+
generator=GeneratorCall(name=name, args=args or ''),
|
58
|
+
finder=finder,
|
59
|
+
)
|
60
|
+
else:
|
61
|
+
stress = package.get_stress(name)
|
62
|
+
|
63
|
+
call = stress.generator
|
64
|
+
generator = package.get_generator(call.name)
|
65
|
+
|
66
|
+
try:
|
67
|
+
generator_digest = compile_item(generator)
|
68
|
+
except:
|
69
|
+
console.console.print(
|
70
|
+
f'[error]Failed compiling generator [item]{generator.name}[/item].[/error]'
|
71
|
+
)
|
72
|
+
raise
|
73
|
+
|
74
|
+
# Finder expression parser
|
75
|
+
parsed_finder = finder_parser.parse(stress.finder)
|
76
|
+
|
77
|
+
solutions = finder_parser.get_all_solution_items(parsed_finder)
|
78
|
+
finders = finder_parser.get_all_checker_items(parsed_finder)
|
79
|
+
needs_expected_output = finder_parser.needs_expected_output(parsed_finder)
|
80
|
+
|
81
|
+
solution_indices = {str(solution.path): i for i, solution in enumerate(solutions)}
|
82
|
+
solutions_digest = compile_solutions(
|
83
|
+
tracked_solutions=set(str(solution.path) for solution in solutions)
|
84
|
+
)
|
85
|
+
if progress:
|
86
|
+
progress.update('Compiling finders...')
|
87
|
+
finders_digest = {str(finder.path): _compile_finder(finder) for finder in finders}
|
88
|
+
|
89
|
+
compiled_validator = validators.compile_main_validator()
|
90
|
+
|
91
|
+
# Erase old stress directory
|
92
|
+
runs_dir = package.get_problem_runs_dir()
|
93
|
+
stress_dir = runs_dir / '.stress'
|
94
|
+
rmtree(str(stress_dir), ignore_errors=True)
|
95
|
+
stress_dir.mkdir(parents=True, exist_ok=True)
|
96
|
+
empty_path = runs_dir / '.stress' / '.empty'
|
97
|
+
empty_path.write_text('')
|
98
|
+
|
99
|
+
startTime = time.monotonic()
|
100
|
+
|
101
|
+
executed = 0
|
102
|
+
findings = []
|
103
|
+
|
104
|
+
while len(findings) < findingsLimit:
|
105
|
+
if time.monotonic() - startTime > timeoutInSeconds:
|
106
|
+
break
|
107
|
+
|
108
|
+
if progress:
|
109
|
+
seconds = timeoutInSeconds - int(time.monotonic() - startTime)
|
110
|
+
progress.update(
|
111
|
+
f'Stress testing: found [item]{len(findings)}[/item] tests, '
|
112
|
+
f'executed [item]{executed}[/item], '
|
113
|
+
f'[item]{seconds}[/item] second(s) remaining...'
|
114
|
+
)
|
115
|
+
|
116
|
+
input_path = runs_dir / '.stress' / 'input'
|
117
|
+
input_path.parent.mkdir(parents=True, exist_ok=True)
|
118
|
+
|
119
|
+
expanded_generator_call = generate_standalone(
|
120
|
+
stress.generator,
|
121
|
+
input_path,
|
122
|
+
generator_digest=generator_digest,
|
123
|
+
validator_digest=compiled_validator[1]
|
124
|
+
if compiled_validator is not None
|
125
|
+
else None,
|
126
|
+
)
|
127
|
+
|
128
|
+
@functools.cache
|
129
|
+
def run_solution_fn(
|
130
|
+
solution: str,
|
131
|
+
input_path=input_path,
|
132
|
+
) -> finder_parser.FinderSolutionResult:
|
133
|
+
index = solution_indices[solution]
|
134
|
+
sol = solutions[index]
|
135
|
+
output_path = input_path.with_stem(f'{index}').with_suffix('.out')
|
136
|
+
stderr_path = output_path.with_suffix('.err')
|
137
|
+
|
138
|
+
run_log = run_item(
|
139
|
+
sol,
|
140
|
+
DigestOrSource.create(solutions_digest[sol.path]),
|
141
|
+
stdin=DigestOrSource.create(input_path),
|
142
|
+
stdout=DigestOrDest.create(output_path),
|
143
|
+
stderr=DigestOrDest.create(stderr_path),
|
144
|
+
)
|
145
|
+
|
146
|
+
return finder_parser.FinderSolutionResult(
|
147
|
+
output_path=output_path,
|
148
|
+
stderr_path=stderr_path,
|
149
|
+
run_log=run_log,
|
150
|
+
)
|
151
|
+
|
152
|
+
# Get main solution output.
|
153
|
+
expected_output_path = empty_path
|
154
|
+
if needs_expected_output:
|
155
|
+
main_result = run_solution_fn(str(solutions[0].path))
|
156
|
+
main_checker_result = checkers.check_with_no_output(main_result.run_log)
|
157
|
+
if main_checker_result.outcome != Outcome.ACCEPTED:
|
158
|
+
console.console.print(
|
159
|
+
'[error]Error while generating main solution output.[/error]'
|
160
|
+
)
|
161
|
+
console.console.print(f'Input written at [item]{input_path}[/item].')
|
162
|
+
console.console.print(
|
163
|
+
f'Output written at [item]{main_result.output_path}[/item].'
|
164
|
+
)
|
165
|
+
console.console.print(
|
166
|
+
f'Stderr written at [item]{main_result.stderr_path}[/item].'
|
167
|
+
)
|
168
|
+
console.console.print()
|
169
|
+
console.console.print(
|
170
|
+
"[warning]If you don't want reference outputs to be generated for the tests, you should "
|
171
|
+
"use the two-way modifier in your finder expression (':2')."
|
172
|
+
)
|
173
|
+
raise typer.Exit(1)
|
174
|
+
expected_output_path = main_result.output_path
|
175
|
+
|
176
|
+
@functools.cache
|
177
|
+
def run_solution_and_checker_fn(
|
178
|
+
solution: str,
|
179
|
+
checker: Optional[finder_parser.FinderChecker],
|
180
|
+
input_path=input_path,
|
181
|
+
expected_output_path=expected_output_path,
|
182
|
+
) -> finder_parser.FinderResult:
|
183
|
+
solution_result = run_solution_fn(solution)
|
184
|
+
|
185
|
+
if checker is None:
|
186
|
+
checker_result = checkers.check_with_no_output(solution_result.run_log)
|
187
|
+
else:
|
188
|
+
checker_digest = finders_digest[checker.path]
|
189
|
+
checker_result = checkers.check(
|
190
|
+
checker_digest,
|
191
|
+
solution_result.run_log,
|
192
|
+
Testcase(inputPath=input_path, outputPath=expected_output_path),
|
193
|
+
program_output=solution_result.output_path,
|
194
|
+
)
|
195
|
+
return finder_parser.FinderResult(
|
196
|
+
solution=solution,
|
197
|
+
outcome=checker_result.outcome,
|
198
|
+
checker=checker,
|
199
|
+
truth_value=True,
|
200
|
+
solution_result=solution_result,
|
201
|
+
checker_result=checker_result,
|
202
|
+
)
|
203
|
+
|
204
|
+
def run_fn(
|
205
|
+
call: finder_parser.FinderCall,
|
206
|
+
) -> finder_parser.FinderResult:
|
207
|
+
finder_result = run_solution_and_checker_fn(call.solution, call.checker)
|
208
|
+
truth_value = call.expected_outcome.match(finder_result.outcome)
|
209
|
+
return dataclasses.replace(finder_result, truth_value=truth_value)
|
210
|
+
|
211
|
+
runner = finder_parser.FinderTreeRunner(runner=run_fn)
|
212
|
+
finder_outcome: finder_parser.FinderOutcome = runner.transform(parsed_finder)
|
213
|
+
|
214
|
+
internal_error_results = [
|
215
|
+
result
|
216
|
+
for result in finder_outcome.results
|
217
|
+
if result.outcome == Outcome.INTERNAL_ERROR
|
218
|
+
]
|
219
|
+
|
220
|
+
if internal_error_results:
|
221
|
+
console.console.print(
|
222
|
+
f'[error]Checkers failed during stress test [item]{name}[/item] with args [info]{expanded_generator_call.name} {expanded_generator_call.args}[/info].[/error]'
|
223
|
+
)
|
224
|
+
for internal_error_result in internal_error_results:
|
225
|
+
assert internal_error_result.checker is not None
|
226
|
+
assert internal_error_result.checker_result is not None
|
227
|
+
internal_error_checker_name = internal_error_result.checker.path
|
228
|
+
console.console.print(
|
229
|
+
f'[warning]Checker [item]{internal_error_checker_name}[/item] failed with message:'
|
230
|
+
)
|
231
|
+
console.console.print(internal_error_result.checker_result.message)
|
232
|
+
raise typer.Exit(1)
|
233
|
+
|
234
|
+
if not finder_outcome.truth_value:
|
235
|
+
continue
|
236
|
+
|
237
|
+
findings_dir = stress_dir / 'findings'
|
238
|
+
findings_dir.mkdir(parents=True, exist_ok=True)
|
239
|
+
finding_index = len(findings)
|
240
|
+
|
241
|
+
finding_path = findings_dir / f'{finding_index}.in'
|
242
|
+
finding_path.write_bytes(input_path.read_bytes())
|
243
|
+
|
244
|
+
if progress:
|
245
|
+
console.console.print(
|
246
|
+
f'[error]FINDING[/error] Generator args are "[status]{expanded_generator_call.name} {expanded_generator_call.args}[/status]"'
|
247
|
+
)
|
248
|
+
seen_finder_results = set()
|
249
|
+
for finder_result in finder_outcome.results:
|
250
|
+
style = get_outcome_style_verdict(finder_result.outcome)
|
251
|
+
finder_result_key = (finder_result.solution, finder_result.checker)
|
252
|
+
if finder_result_key in seen_finder_results:
|
253
|
+
continue
|
254
|
+
seen_finder_results.add(finder_result_key)
|
255
|
+
finder_result_report_line = f'{finder_result.solution} = [{style}]{finder_result.outcome.name}[/{style}]'
|
256
|
+
if finder_result.checker is not None:
|
257
|
+
finder_result_report_line += (
|
258
|
+
f' [item]ON[/item] {finder_result.checker.path}'
|
259
|
+
)
|
260
|
+
console.console.print(finder_result_report_line)
|
261
|
+
|
262
|
+
findings.append(
|
263
|
+
StressFinding(
|
264
|
+
generator=expanded_generator_call,
|
265
|
+
)
|
266
|
+
)
|
267
|
+
|
268
|
+
# Be cooperative.
|
269
|
+
executed += 1
|
270
|
+
time.sleep(0.001)
|
271
|
+
|
272
|
+
return StressReport(findings=findings, executed=executed)
|
273
|
+
|
274
|
+
|
275
|
+
def print_stress_report(report: StressReport):
|
276
|
+
console.console.rule('Stress test report', style='status')
|
277
|
+
console.console.print(f'Executed [item]{report.executed}[/item] tests.')
|
278
|
+
if not report.findings:
|
279
|
+
console.console.print('No stress test findings.')
|
280
|
+
return
|
281
|
+
console.console.print(f'Found [item]{len(report.findings)}[/item] testcases.')
|
282
|
+
|
283
|
+
findings_dir = package.get_problem_runs_dir() / '.stress' / 'findings'
|
284
|
+
console.console.print(f'Findings: {findings_dir.resolve()}')
|
285
|
+
console.console.print()
|
286
|
+
|
287
|
+
for i, finding in enumerate(report.findings):
|
288
|
+
console.console.print(f'[error]Finding {i + 1}[/error]')
|
289
|
+
console.console.print(
|
290
|
+
f'Generator: [status]{finding.generator.name} {finding.generator.args}[/status]'
|
291
|
+
)
|
292
|
+
console.console.print()
|
File without changes
|