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,359 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import pathlib
|
3
|
+
import typing
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Callable, List, Optional
|
6
|
+
|
7
|
+
import lark
|
8
|
+
import typer
|
9
|
+
|
10
|
+
from rbx import console
|
11
|
+
from rbx.box import package
|
12
|
+
from rbx.box.schema import CodeItem, ExpectedOutcome
|
13
|
+
from rbx.grading.steps import CheckerResult, Outcome, RunLog
|
14
|
+
|
15
|
+
LARK_GRAMMAR = r"""
|
16
|
+
// A bunch of words
|
17
|
+
start: disjunction
|
18
|
+
|
19
|
+
disjunction: conjunction | disjunction _OR conjunction
|
20
|
+
|
21
|
+
conjunction: _atom | conjunction _AND _atom
|
22
|
+
|
23
|
+
_atom: statement | "(" disjunction ")" | negation
|
24
|
+
negation: _NOT "(" disjunction ")"
|
25
|
+
|
26
|
+
statement: solution matcher outcome checking?
|
27
|
+
|
28
|
+
solution: _filename | WILDCARD
|
29
|
+
outcome: CNAME
|
30
|
+
checking: "ON"i (checking_mode? checker | ":nil")
|
31
|
+
checking_mode: MODE ":"
|
32
|
+
MODE: "2" | "3"
|
33
|
+
checker: _filename | WILDCARD
|
34
|
+
|
35
|
+
// Operators
|
36
|
+
matcher: MATCHES | NOT_MATCHES
|
37
|
+
|
38
|
+
MATCHES: "~"
|
39
|
+
NOT_MATCHES: "!~"
|
40
|
+
_OR: "||"
|
41
|
+
_AND: "&&"
|
42
|
+
_NOT: "!"
|
43
|
+
WILDCARD: "$"
|
44
|
+
|
45
|
+
// File name
|
46
|
+
_filename: FILENAME | "\"" FILENAME "\""
|
47
|
+
FILENAME: /[\/A-Za-z0-9\-_\.]/+
|
48
|
+
|
49
|
+
%import common.CNAME
|
50
|
+
%ignore " "
|
51
|
+
"""
|
52
|
+
|
53
|
+
LARK_PARSER = lark.Lark(LARK_GRAMMAR)
|
54
|
+
|
55
|
+
|
56
|
+
class CheckingMode(Enum):
|
57
|
+
THREE_WAY = 0
|
58
|
+
TWO_WAY = 1
|
59
|
+
|
60
|
+
|
61
|
+
@dataclasses.dataclass(frozen=True)
|
62
|
+
class FinderChecker:
|
63
|
+
path: str
|
64
|
+
mode: CheckingMode
|
65
|
+
|
66
|
+
|
67
|
+
@dataclasses.dataclass(frozen=True)
|
68
|
+
class FinderCall:
|
69
|
+
solution: str
|
70
|
+
expected_outcome: ExpectedOutcome
|
71
|
+
checker: Optional[FinderChecker]
|
72
|
+
|
73
|
+
|
74
|
+
@dataclasses.dataclass(frozen=True)
|
75
|
+
class FinderSolutionResult:
|
76
|
+
output_path: pathlib.Path
|
77
|
+
stderr_path: Optional[pathlib.Path] = None
|
78
|
+
run_log: Optional[RunLog] = None
|
79
|
+
|
80
|
+
|
81
|
+
@dataclasses.dataclass(frozen=True)
|
82
|
+
class FinderResult:
|
83
|
+
solution: str
|
84
|
+
outcome: Outcome
|
85
|
+
checker: Optional[FinderChecker]
|
86
|
+
truth_value: bool
|
87
|
+
|
88
|
+
# Auxiliary information.
|
89
|
+
solution_result: Optional[FinderSolutionResult] = None
|
90
|
+
checker_result: Optional[CheckerResult] = None
|
91
|
+
|
92
|
+
|
93
|
+
@dataclasses.dataclass(frozen=True)
|
94
|
+
class FinderOutcome:
|
95
|
+
truth_value: bool
|
96
|
+
results: List[FinderResult]
|
97
|
+
|
98
|
+
|
99
|
+
def or_outcome(a: FinderOutcome, b: FinderOutcome) -> FinderOutcome:
|
100
|
+
return FinderOutcome(
|
101
|
+
truth_value=a.truth_value or b.truth_value, results=a.results + b.results
|
102
|
+
)
|
103
|
+
|
104
|
+
|
105
|
+
def and_outcome(a: FinderOutcome, b: FinderOutcome) -> FinderOutcome:
|
106
|
+
return FinderOutcome(
|
107
|
+
truth_value=a.truth_value and b.truth_value, results=a.results + b.results
|
108
|
+
)
|
109
|
+
|
110
|
+
|
111
|
+
def get_checking_mode_from_string(mode: Optional[str]) -> CheckingMode:
|
112
|
+
if not mode:
|
113
|
+
return CheckingMode.THREE_WAY
|
114
|
+
if mode == '2':
|
115
|
+
return CheckingMode.TWO_WAY
|
116
|
+
return CheckingMode.THREE_WAY
|
117
|
+
|
118
|
+
|
119
|
+
def _get_main_checker() -> Optional[str]:
|
120
|
+
pkg = package.find_problem_package_or_die()
|
121
|
+
if not pkg.checker:
|
122
|
+
return None
|
123
|
+
return str(pkg.checker.path)
|
124
|
+
|
125
|
+
|
126
|
+
def _get_main_solution() -> Optional[str]:
|
127
|
+
sol = package.get_main_solution()
|
128
|
+
if sol is None:
|
129
|
+
return None
|
130
|
+
return str(sol.path)
|
131
|
+
|
132
|
+
|
133
|
+
def _get_default_checker_for_finder() -> Optional[FinderChecker]:
|
134
|
+
main_checker = _get_main_checker()
|
135
|
+
if main_checker is None:
|
136
|
+
return None
|
137
|
+
return FinderChecker(path=main_checker, mode=CheckingMode.THREE_WAY)
|
138
|
+
|
139
|
+
|
140
|
+
def _get_solution_from_token(token: lark.Token) -> str:
|
141
|
+
path = str(token)
|
142
|
+
if path == '$':
|
143
|
+
main_path = _get_main_solution()
|
144
|
+
assert main_path is not None
|
145
|
+
return main_path
|
146
|
+
return path
|
147
|
+
|
148
|
+
|
149
|
+
def _get_checker_from_token(token: lark.Token) -> str:
|
150
|
+
path = str(token)
|
151
|
+
if path == '$':
|
152
|
+
main_path = _get_main_checker()
|
153
|
+
assert main_path is not None
|
154
|
+
return main_path
|
155
|
+
return path
|
156
|
+
|
157
|
+
|
158
|
+
def _get_statement_checker(statement: lark.ParseTree) -> Optional[FinderChecker]:
|
159
|
+
checking_nodes = list(statement.find_data('checking'))
|
160
|
+
if not checking_nodes:
|
161
|
+
return _get_default_checker_for_finder()
|
162
|
+
(checking,) = checking_nodes
|
163
|
+
|
164
|
+
if not checking.children:
|
165
|
+
# Checking is nil
|
166
|
+
return None
|
167
|
+
|
168
|
+
if len(checking.children) == 1:
|
169
|
+
checker = typing.cast(lark.ParseTree, checking.children[0])
|
170
|
+
return FinderChecker(
|
171
|
+
path=_get_checker_from_token(typing.cast(lark.Token, checker.children[0])),
|
172
|
+
mode=CheckingMode.THREE_WAY,
|
173
|
+
)
|
174
|
+
|
175
|
+
mode = typing.cast(lark.ParseTree, checking.children[0])
|
176
|
+
checker = typing.cast(lark.ParseTree, checking.children[1])
|
177
|
+
|
178
|
+
return FinderChecker(
|
179
|
+
path=_get_checker_from_token(typing.cast(lark.Token, checker.children[0])),
|
180
|
+
mode=get_checking_mode_from_string(
|
181
|
+
typing.cast(lark.Token, mode.children[0]).value
|
182
|
+
),
|
183
|
+
)
|
184
|
+
|
185
|
+
|
186
|
+
def get_all_solutions(tree: lark.ParseTree) -> List[str]:
|
187
|
+
solution_nodes = tree.find_data('solution')
|
188
|
+
res = set(
|
189
|
+
[
|
190
|
+
_get_solution_from_token(typing.cast(lark.Token, node.children[0]))
|
191
|
+
for node in solution_nodes
|
192
|
+
]
|
193
|
+
)
|
194
|
+
|
195
|
+
if needs_expected_output(tree):
|
196
|
+
main_solution = package.get_main_solution()
|
197
|
+
assert main_solution is not None
|
198
|
+
res.add(str(main_solution.path))
|
199
|
+
return list(res)
|
200
|
+
|
201
|
+
|
202
|
+
def get_all_solution_items(tree: lark.ParseTree) -> List[CodeItem]:
|
203
|
+
solution_names = get_all_solutions(tree)
|
204
|
+
res = []
|
205
|
+
|
206
|
+
for solution_name in solution_names:
|
207
|
+
found_solution = package.get_solution_or_nil(solution_name)
|
208
|
+
if found_solution is None:
|
209
|
+
res.append(
|
210
|
+
CodeItem(
|
211
|
+
path=pathlib.Path(solution_name),
|
212
|
+
language=None,
|
213
|
+
compilationFiles=None,
|
214
|
+
)
|
215
|
+
)
|
216
|
+
continue
|
217
|
+
res.append(found_solution)
|
218
|
+
|
219
|
+
main_solution = package.get_main_solution()
|
220
|
+
if main_solution is None:
|
221
|
+
return res
|
222
|
+
|
223
|
+
for i, sol in enumerate(res):
|
224
|
+
if main_solution.path == sol.path:
|
225
|
+
res[i], res[0] = res[0], res[i]
|
226
|
+
return res
|
227
|
+
|
228
|
+
|
229
|
+
def _get_all_finder_checkers(tree: lark.ParseTree) -> List[FinderChecker]:
|
230
|
+
statement_nodes = tree.find_data('statement')
|
231
|
+
res = []
|
232
|
+
|
233
|
+
for statement_node in statement_nodes:
|
234
|
+
finder_checker = _get_statement_checker(statement_node)
|
235
|
+
if finder_checker is not None:
|
236
|
+
res.append(finder_checker)
|
237
|
+
|
238
|
+
return res
|
239
|
+
|
240
|
+
|
241
|
+
def get_all_checkers(tree: lark.ParseTree) -> List[str]:
|
242
|
+
return [finder_checker.path for finder_checker in _get_all_finder_checkers(tree)]
|
243
|
+
|
244
|
+
|
245
|
+
def get_all_checker_items(tree: lark.ParseTree) -> List[CodeItem]:
|
246
|
+
checker_names = get_all_checkers(tree)
|
247
|
+
res = []
|
248
|
+
|
249
|
+
for checker_name in checker_names:
|
250
|
+
main_checker = package.get_checker()
|
251
|
+
if str(main_checker.path) == checker_name:
|
252
|
+
res.append(main_checker)
|
253
|
+
continue
|
254
|
+
res.append(
|
255
|
+
CodeItem(
|
256
|
+
path=pathlib.Path(checker_name),
|
257
|
+
language=None,
|
258
|
+
compilationFiles=None,
|
259
|
+
)
|
260
|
+
)
|
261
|
+
return res
|
262
|
+
|
263
|
+
|
264
|
+
def needs_expected_output(tree: lark.ParseTree) -> bool:
|
265
|
+
finder_checkers = _get_all_finder_checkers(tree)
|
266
|
+
for finder_checker in finder_checkers:
|
267
|
+
if finder_checker.mode == CheckingMode.THREE_WAY:
|
268
|
+
return True
|
269
|
+
return False
|
270
|
+
|
271
|
+
|
272
|
+
def validate(tree: lark.ParseTree):
|
273
|
+
if needs_expected_output(tree):
|
274
|
+
if package.get_main_solution() is None:
|
275
|
+
console.console.print(
|
276
|
+
'[error]Finder expression requires three-way checking, but problem has no main solution.[/error]'
|
277
|
+
)
|
278
|
+
console.console.print(
|
279
|
+
'Either provide an ACCEPTED solution at your problem.rbx.yml, or use two-way checking in your finder expression by providing the `2:` parameter before the checker name.'
|
280
|
+
)
|
281
|
+
raise typer.Exit(1)
|
282
|
+
|
283
|
+
all_checkers = get_all_checkers(tree)
|
284
|
+
for checker in all_checkers:
|
285
|
+
if not pathlib.Path(checker).is_file():
|
286
|
+
console.console.print(
|
287
|
+
f'[error]Finder expression references non-existing checker [item]{checker}[/item].[/error]'
|
288
|
+
)
|
289
|
+
raise typer.Exit(1)
|
290
|
+
|
291
|
+
all_solutions = get_all_solutions(tree)
|
292
|
+
for solution in all_solutions:
|
293
|
+
if not pathlib.Path(solution).is_file():
|
294
|
+
console.console.print(
|
295
|
+
f'[error]Finder expression references non-existing solution [item]{solution}[/item].[/error]'
|
296
|
+
)
|
297
|
+
raise typer.Exit(1)
|
298
|
+
|
299
|
+
|
300
|
+
@lark.v_args(inline=True)
|
301
|
+
class FinderTreeRunner(lark.Transformer):
|
302
|
+
outcome = ExpectedOutcome
|
303
|
+
|
304
|
+
def __init__(
|
305
|
+
self,
|
306
|
+
runner: Callable[[FinderCall], FinderResult],
|
307
|
+
):
|
308
|
+
self.run_fn = runner
|
309
|
+
|
310
|
+
def solution(self, token: lark.Token) -> str:
|
311
|
+
return _get_solution_from_token(token)
|
312
|
+
|
313
|
+
def matcher(self, op: lark.Token) -> bool:
|
314
|
+
return op.value == '~'
|
315
|
+
|
316
|
+
@lark.v_args(inline=False, tree=True)
|
317
|
+
def statement(
|
318
|
+
self,
|
319
|
+
tree: lark.ParseTree,
|
320
|
+
) -> FinderOutcome:
|
321
|
+
solution = typing.cast(str, tree.children[0])
|
322
|
+
is_positive = typing.cast(bool, tree.children[1])
|
323
|
+
expected_outcome = typing.cast(ExpectedOutcome, tree.children[2])
|
324
|
+
|
325
|
+
checker: Optional[FinderChecker] = _get_statement_checker(tree)
|
326
|
+
|
327
|
+
call = FinderCall(solution, expected_outcome=expected_outcome, checker=checker)
|
328
|
+
result = self.run_fn(call)
|
329
|
+
truth_value = result.truth_value
|
330
|
+
if not is_positive:
|
331
|
+
truth_value = not truth_value
|
332
|
+
|
333
|
+
return FinderOutcome(truth_value=truth_value, results=[result])
|
334
|
+
|
335
|
+
def negation(self, value: FinderOutcome) -> FinderOutcome:
|
336
|
+
return dataclasses.replace(value, truth_value=not value.truth_value)
|
337
|
+
|
338
|
+
@lark.v_args(inline=False)
|
339
|
+
def conjunction(self, values: List[FinderOutcome]) -> FinderOutcome:
|
340
|
+
res = FinderOutcome(truth_value=True, results=[])
|
341
|
+
for value in values:
|
342
|
+
res = and_outcome(res, value)
|
343
|
+
return res
|
344
|
+
|
345
|
+
@lark.v_args(inline=False)
|
346
|
+
def disjunction(self, values: List[FinderOutcome]) -> FinderOutcome:
|
347
|
+
res = FinderOutcome(truth_value=False, results=[])
|
348
|
+
for value in values:
|
349
|
+
res = or_outcome(res, value)
|
350
|
+
return res
|
351
|
+
|
352
|
+
def start(self, value: FinderOutcome) -> FinderOutcome:
|
353
|
+
return value
|
354
|
+
|
355
|
+
|
356
|
+
def parse(expression: str) -> lark.ParseTree:
|
357
|
+
tree = LARK_PARSER.parse(expression)
|
358
|
+
validate(tree)
|
359
|
+
return tree
|
@@ -0,0 +1,258 @@
|
|
1
|
+
# flake8: noqa
|
2
|
+
from typing import Any, Optional, Union
|
3
|
+
import typing
|
4
|
+
|
5
|
+
import random
|
6
|
+
import lark
|
7
|
+
|
8
|
+
LARK_GRAMMAR = r"""
|
9
|
+
start: args
|
10
|
+
|
11
|
+
// A bunch of args
|
12
|
+
args: (arg (_WS arg)*)?
|
13
|
+
|
14
|
+
// Argument shlex
|
15
|
+
arg: _block | random_hex
|
16
|
+
|
17
|
+
// Blocks
|
18
|
+
_block: (TEXT | _ticked | _expr)+
|
19
|
+
|
20
|
+
// Expression
|
21
|
+
_expr: var | range | select
|
22
|
+
|
23
|
+
// Ticked
|
24
|
+
_ticked: "`" _expr "`"
|
25
|
+
|
26
|
+
// Variables
|
27
|
+
var: "<" CNAME ">"
|
28
|
+
|
29
|
+
// Select
|
30
|
+
select: "(" select_value ("|" select_value)* ")"
|
31
|
+
select_value: _block
|
32
|
+
|
33
|
+
// Ranges
|
34
|
+
range: "[" range_value ".." range_value "]"
|
35
|
+
range_value: range | var | int | float | char
|
36
|
+
int: SIGNED_INT
|
37
|
+
float: SIGNED_FLOAT
|
38
|
+
char: "'" /./ "'"
|
39
|
+
|
40
|
+
// Random hex
|
41
|
+
random_hex.1: "@"
|
42
|
+
|
43
|
+
// Rest, strings, etc
|
44
|
+
TEXT: (/[^ \t\f\r\n\[\]\(\)\<\>\|\`]/ | ESCAPED_STRING)+
|
45
|
+
|
46
|
+
// Whitespace
|
47
|
+
_WS: WS
|
48
|
+
|
49
|
+
%import common.WS
|
50
|
+
%import common.CNAME
|
51
|
+
%import common.SIGNED_INT
|
52
|
+
%import common.SIGNED_FLOAT
|
53
|
+
%import common.ESCAPED_STRING
|
54
|
+
"""
|
55
|
+
|
56
|
+
LARK_PARSER = lark.Lark(LARK_GRAMMAR)
|
57
|
+
|
58
|
+
Primitive = Union[int, float, str]
|
59
|
+
|
60
|
+
|
61
|
+
class GeneratorParsingError(Exception):
|
62
|
+
pass
|
63
|
+
|
64
|
+
|
65
|
+
class RandomInt:
|
66
|
+
def __init__(self, min: int, max: int):
|
67
|
+
self.min = min
|
68
|
+
self.max = max
|
69
|
+
|
70
|
+
def get(self) -> int:
|
71
|
+
if self.max < self.min:
|
72
|
+
raise GeneratorParsingError(
|
73
|
+
f'Found int range with invalid bounds [{self.min}..{self.max}].'
|
74
|
+
)
|
75
|
+
return random.randint(self.min, self.max)
|
76
|
+
|
77
|
+
|
78
|
+
class RandomChar:
|
79
|
+
def __init__(self, min: str, max: str):
|
80
|
+
self.min = min
|
81
|
+
self.max = max
|
82
|
+
|
83
|
+
def get(self) -> str:
|
84
|
+
if len(self.min) != 1 or len(self.max) != 1:
|
85
|
+
raise GeneratorParsingError(
|
86
|
+
f"Found char range with invalid bounds ['{self.min}'..'{self.max}']"
|
87
|
+
)
|
88
|
+
mn = ord(self.min)
|
89
|
+
mx = ord(self.max)
|
90
|
+
if mx < mn:
|
91
|
+
raise GeneratorParsingError(
|
92
|
+
f"Found char range with invalid bounds ['{self.min}'..'{self.max}']"
|
93
|
+
)
|
94
|
+
return chr(random.randint(mn, mx))
|
95
|
+
|
96
|
+
|
97
|
+
class RandomHex:
|
98
|
+
len: int
|
99
|
+
|
100
|
+
def __init__(self, len: int = 8):
|
101
|
+
self.len = len
|
102
|
+
|
103
|
+
def get(self) -> str:
|
104
|
+
return ''.join(random.choice('0123456789abcdef') for _ in range(self.len))
|
105
|
+
|
106
|
+
|
107
|
+
def parse(args: str) -> lark.ParseTree:
|
108
|
+
tree = LARK_PARSER.parse(args)
|
109
|
+
(args_root,) = tree.find_data('args')
|
110
|
+
return args_root
|
111
|
+
|
112
|
+
|
113
|
+
def _var_as_str(var: Any) -> str:
|
114
|
+
if isinstance(var, float):
|
115
|
+
return f'{var:.6f}'
|
116
|
+
return str(var)
|
117
|
+
|
118
|
+
|
119
|
+
def _down(node: lark.ParseTree) -> Union[lark.Token, lark.ParseTree]:
|
120
|
+
return node.children[0]
|
121
|
+
|
122
|
+
|
123
|
+
def _down_tree(node: lark.ParseTree) -> lark.ParseTree:
|
124
|
+
downed = _down(node)
|
125
|
+
return typing.cast(lark.ParseTree, downed)
|
126
|
+
|
127
|
+
|
128
|
+
def _down_token(node: lark.ParseTree) -> lark.Token:
|
129
|
+
downed = _down(node)
|
130
|
+
return typing.cast(lark.Token, downed)
|
131
|
+
|
132
|
+
|
133
|
+
def _is_primitive(x: Primitive) -> bool:
|
134
|
+
return isinstance(x, (int, float, str))
|
135
|
+
|
136
|
+
|
137
|
+
def _get_casting_type(a: Primitive, b: Primitive) -> Optional[str]:
|
138
|
+
if isinstance(a, int) and isinstance(b, int):
|
139
|
+
return 'int'
|
140
|
+
if isinstance(a, float) or isinstance(b, float):
|
141
|
+
if isinstance(a, (int, float)) or isinstance(b, (int, float)):
|
142
|
+
return 'float'
|
143
|
+
if isinstance(a, str) and isinstance(b, str):
|
144
|
+
return 'char'
|
145
|
+
return None
|
146
|
+
|
147
|
+
|
148
|
+
class Generator:
|
149
|
+
def __init__(self, vars):
|
150
|
+
self.vars = vars
|
151
|
+
|
152
|
+
def handle_var(self, expr: lark.ParseTree) -> Any:
|
153
|
+
name = typing.cast(lark.Token, _down(expr))
|
154
|
+
if name.value not in self.vars:
|
155
|
+
raise GeneratorParsingError(
|
156
|
+
f'Error parsing generator expression: variable {name.value} is not defined'
|
157
|
+
)
|
158
|
+
value = self.vars[name.value]
|
159
|
+
if not _is_primitive(value):
|
160
|
+
raise GeneratorParsingError(
|
161
|
+
f'Variable {name.value} has type {type(value)}, which is not supported by the Generator expression parser.'
|
162
|
+
)
|
163
|
+
return value
|
164
|
+
|
165
|
+
def handle_range_value(self, range_value: lark.ParseTree) -> Primitive:
|
166
|
+
tp = _down_tree(range_value)
|
167
|
+
if tp.data == 'int':
|
168
|
+
return int(_down_token(tp).value)
|
169
|
+
if tp.data == 'float':
|
170
|
+
return float(_down_token(tp).value)
|
171
|
+
if tp.data == 'char':
|
172
|
+
return str(_down_token(tp).value)
|
173
|
+
if tp.data == 'var':
|
174
|
+
return self.handle_var(tp)
|
175
|
+
return self.handle_range(tp)
|
176
|
+
|
177
|
+
def handle_range(self, range: lark.ParseTree) -> Primitive:
|
178
|
+
value_a, value_b = range.children[:2]
|
179
|
+
|
180
|
+
item_a = self.handle_range_value(typing.cast(lark.ParseTree, value_a))
|
181
|
+
item_b = self.handle_range_value(typing.cast(lark.ParseTree, value_b))
|
182
|
+
|
183
|
+
casting_type = _get_casting_type(item_a, item_b)
|
184
|
+
if casting_type is None:
|
185
|
+
raise GeneratorParsingError(
|
186
|
+
f'Types in range are uncompatible: {type(item_a)} != {type(item_b)}'
|
187
|
+
)
|
188
|
+
|
189
|
+
if casting_type == 'int':
|
190
|
+
return RandomInt(int(item_a), int(item_b)).get()
|
191
|
+
if casting_type == 'float':
|
192
|
+
return random.uniform(float(item_a), float(item_b))
|
193
|
+
if casting_type == 'char':
|
194
|
+
return RandomChar(str(item_a), str(item_b)).get()
|
195
|
+
|
196
|
+
raise GeneratorParsingError(
|
197
|
+
f'Types in range are not supported: {type(item_a)}, {type(item_b)}'
|
198
|
+
)
|
199
|
+
|
200
|
+
def handle_select_value(self, select_value: lark.ParseTree) -> str:
|
201
|
+
items = []
|
202
|
+
for item in select_value.children:
|
203
|
+
items.append(self.handle_block(item))
|
204
|
+
return ''.join(items)
|
205
|
+
|
206
|
+
def handle_select(self, select: lark.ParseTree) -> str:
|
207
|
+
options = []
|
208
|
+
for select_value in select.children:
|
209
|
+
options.append(
|
210
|
+
self.handle_select_value(typing.cast(lark.ParseTree, select_value))
|
211
|
+
)
|
212
|
+
return options[RandomInt(0, len(options) - 1).get()]
|
213
|
+
|
214
|
+
def handle_expr(self, expr: lark.ParseTree) -> str:
|
215
|
+
if expr.data == 'var':
|
216
|
+
return _var_as_str(self.handle_var(expr))
|
217
|
+
if expr.data == 'range':
|
218
|
+
return _var_as_str(self.handle_range(expr))
|
219
|
+
if expr.data == 'select':
|
220
|
+
return self.handle_select(expr)
|
221
|
+
raise GeneratorParsingError(
|
222
|
+
f'Internal error: found invalid AST node in Generator parsing, {expr.data}'
|
223
|
+
)
|
224
|
+
|
225
|
+
def handle_block(self, block: Union[lark.ParseTree, lark.Token]) -> str:
|
226
|
+
if isinstance(block, lark.Token):
|
227
|
+
return block.value
|
228
|
+
|
229
|
+
return self.handle_expr(block)
|
230
|
+
|
231
|
+
def generate_arg(self, arg: lark.ParseTree) -> str:
|
232
|
+
items = []
|
233
|
+
for arg_item in arg.children:
|
234
|
+
if hasattr(arg_item, 'data'):
|
235
|
+
data_item = typing.cast(lark.ParseTree, arg_item)
|
236
|
+
if data_item.data == 'random_hex':
|
237
|
+
items.append(RandomHex().get())
|
238
|
+
continue
|
239
|
+
items.append(self.handle_block(arg_item))
|
240
|
+
return ''.join(items)
|
241
|
+
|
242
|
+
def generate(self, args: lark.ParseTree) -> str:
|
243
|
+
args_list = []
|
244
|
+
|
245
|
+
for arg in args.children:
|
246
|
+
args_list.append(self.generate_arg(typing.cast(lark.ParseTree, arg)))
|
247
|
+
|
248
|
+
return ' '.join(args_list)
|
249
|
+
|
250
|
+
|
251
|
+
if __name__ == '__main__':
|
252
|
+
tree = parse(
|
253
|
+
"""--MAX_N="allow me" --int=[1..<MAX_N>] --float=[1.0..<MAX_N>] --char=['a'..'a'] --selection="(a|b|(c|<MAX_N>))" --select=("a"|"b"|"c") @ --r2=[1..[8..15]]"""
|
254
|
+
)
|
255
|
+
print(tree)
|
256
|
+
|
257
|
+
generator = Generator({'MAX_N': 10})
|
258
|
+
print(generator.generate(tree))
|
rbx/box/testcases.py
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
import pathlib
|
2
|
+
import shutil
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
import typer
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
from rbx import console
|
9
|
+
from rbx.box import package
|
10
|
+
from rbx.box.package import get_build_testgroup_path, get_build_tests_path
|
11
|
+
from rbx.box.schema import Testcase, TestcaseGroup
|
12
|
+
|
13
|
+
|
14
|
+
class TestcaseData(BaseModel):
|
15
|
+
input: str
|
16
|
+
output: str
|
17
|
+
|
18
|
+
|
19
|
+
def find_built_testcases(group: TestcaseGroup) -> List[Testcase]:
|
20
|
+
inputs = find_built_testcase_inputs(group)
|
21
|
+
|
22
|
+
testcases = []
|
23
|
+
for input in inputs:
|
24
|
+
output = input.with_suffix('.out')
|
25
|
+
testcases.append(Testcase(inputPath=input, outputPath=output))
|
26
|
+
return testcases
|
27
|
+
|
28
|
+
|
29
|
+
def find_built_testcase_inputs(group: TestcaseGroup) -> List[pathlib.Path]:
|
30
|
+
testgroup_path = get_build_testgroup_path(group.name)
|
31
|
+
if not testgroup_path.is_dir():
|
32
|
+
console.console.print(
|
33
|
+
f'Testgroup {group.name} is not generated in build folder'
|
34
|
+
)
|
35
|
+
raise typer.Exit(1)
|
36
|
+
|
37
|
+
return sorted(testgroup_path.glob('*.in'))
|
38
|
+
|
39
|
+
|
40
|
+
def clear_built_testcases():
|
41
|
+
shutil.rmtree(str(get_build_tests_path()), ignore_errors=True)
|
42
|
+
|
43
|
+
|
44
|
+
def get_samples() -> List[Testcase]:
|
45
|
+
tcs = find_built_testcases(package.get_testgroup('samples'))
|
46
|
+
return [
|
47
|
+
Testcase(
|
48
|
+
inputPath=tc.inputPath.resolve(),
|
49
|
+
outputPath=tc.outputPath.resolve()
|
50
|
+
if tc.outputPath is not None and tc.outputPath.is_file()
|
51
|
+
else None,
|
52
|
+
)
|
53
|
+
for tc in tcs
|
54
|
+
]
|
rbx/box/ui/__init__.py
ADDED
File without changes
|