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.
Files changed (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. 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