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,372 @@
|
|
1
|
+
import asyncio
|
2
|
+
import copy
|
3
|
+
import dataclasses
|
4
|
+
import fcntl
|
5
|
+
import os
|
6
|
+
import pty
|
7
|
+
import re
|
8
|
+
import signal
|
9
|
+
import struct
|
10
|
+
import termios
|
11
|
+
from typing import Callable, List, Optional
|
12
|
+
|
13
|
+
import pyte
|
14
|
+
import textual
|
15
|
+
from pyte.screens import Char
|
16
|
+
from rich.color import ColorParseError
|
17
|
+
from rich.segment import Segment
|
18
|
+
from rich.style import Style
|
19
|
+
from rich.text import Text
|
20
|
+
from textual import events
|
21
|
+
from textual.app import DEFAULT_COLORS
|
22
|
+
from textual.design import ColorSystem
|
23
|
+
from textual.geometry import Size
|
24
|
+
from textual.scroll_view import ScrollView
|
25
|
+
from textual.strip import Strip
|
26
|
+
|
27
|
+
|
28
|
+
class PyteDisplay:
|
29
|
+
lines: List[List[Segment]]
|
30
|
+
|
31
|
+
def __init__(self, lines):
|
32
|
+
self.lines = lines
|
33
|
+
|
34
|
+
@property
|
35
|
+
def virtual_height(self):
|
36
|
+
return len(self.lines)
|
37
|
+
|
38
|
+
|
39
|
+
@dataclasses.dataclass
|
40
|
+
class LogDisplayState:
|
41
|
+
screen: pyte.Screen
|
42
|
+
exitcode: Optional[int]
|
43
|
+
|
44
|
+
|
45
|
+
@dataclasses.dataclass
|
46
|
+
class Emulator:
|
47
|
+
communicate_task: asyncio.Task
|
48
|
+
send_task: asyncio.Task
|
49
|
+
wait_task: asyncio.Task
|
50
|
+
pid: int
|
51
|
+
callback: Optional[Callable] = None
|
52
|
+
|
53
|
+
def disconnect(self):
|
54
|
+
self.communicate_task.cancel()
|
55
|
+
self.send_task.cancel()
|
56
|
+
self.wait_task.cancel()
|
57
|
+
try:
|
58
|
+
os.kill(self.pid, signal.SIGTERM)
|
59
|
+
except OSError:
|
60
|
+
# Process does not exist anymore.
|
61
|
+
pass
|
62
|
+
|
63
|
+
|
64
|
+
class LogDisplay(ScrollView, can_focus=True):
|
65
|
+
DEFAULT_CSS = """
|
66
|
+
LogDisplay {
|
67
|
+
background: $background;
|
68
|
+
}
|
69
|
+
"""
|
70
|
+
emulator: Optional[Emulator]
|
71
|
+
exitcode: Optional[int]
|
72
|
+
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
default_colors: Optional[str] = 'textual',
|
76
|
+
max_lines: int = 1000,
|
77
|
+
name: Optional[str] = None,
|
78
|
+
id: Optional[str] = None,
|
79
|
+
classes: Optional[str] = None,
|
80
|
+
):
|
81
|
+
super().__init__(name=name, id=id, classes=classes)
|
82
|
+
|
83
|
+
self.emulator = None
|
84
|
+
self.default_colors = default_colors
|
85
|
+
if default_colors == 'textual':
|
86
|
+
self.textual_colors = self.detect_textual_colors()
|
87
|
+
|
88
|
+
self.virtual_size = Size(80, max_lines)
|
89
|
+
self._max_lines = max_lines
|
90
|
+
self._display = PyteDisplay([Text()])
|
91
|
+
self._screen = pyte.Screen(self.virtual_size.width, self._max_lines)
|
92
|
+
self.stream = pyte.Stream(self._screen)
|
93
|
+
|
94
|
+
self.recv_queue = asyncio.Queue()
|
95
|
+
self.send_queue = asyncio.Queue()
|
96
|
+
self.exitcode = None
|
97
|
+
|
98
|
+
async def on_resize(self, _event: events.Resize):
|
99
|
+
if self.emulator is None:
|
100
|
+
return
|
101
|
+
|
102
|
+
# Update only width.
|
103
|
+
self.virtual_size = Size(
|
104
|
+
width=self.size.width - 2, # Account for scroll bar.
|
105
|
+
height=self.virtual_size.height,
|
106
|
+
)
|
107
|
+
await self.send_queue.put(
|
108
|
+
['set_size', self._max_lines, self.virtual_size.width]
|
109
|
+
)
|
110
|
+
self._screen.resize(self._max_lines, self.virtual_size.width)
|
111
|
+
self.update_display()
|
112
|
+
|
113
|
+
def update_display(self):
|
114
|
+
lines: List[List[Segment]] = []
|
115
|
+
for y in range(self._screen.lines):
|
116
|
+
line = self._screen.buffer[y]
|
117
|
+
line_segments = []
|
118
|
+
accumulated_text = []
|
119
|
+
for x in range(self._screen.columns):
|
120
|
+
char: Char = line[x]
|
121
|
+
if x > 0 and (not self.char_style_cmp(char, line[x - 1])):
|
122
|
+
text = ''.join(accumulated_text)
|
123
|
+
line_segments.append(
|
124
|
+
Segment(text, style=self.char_rich_style(line[x - 1]))
|
125
|
+
)
|
126
|
+
accumulated_text = []
|
127
|
+
accumulated_text.append(char.data)
|
128
|
+
if accumulated_text:
|
129
|
+
text = ''.join(accumulated_text)
|
130
|
+
line_segments.append(
|
131
|
+
Segment(
|
132
|
+
text, style=self.char_rich_style(line[self._screen.columns - 1])
|
133
|
+
)
|
134
|
+
)
|
135
|
+
lines.append(line_segments)
|
136
|
+
|
137
|
+
# Remove empty lines from the back.
|
138
|
+
while lines:
|
139
|
+
last_line = lines[-1]
|
140
|
+
text = ''.join(seg.text for seg in last_line)
|
141
|
+
if text.strip():
|
142
|
+
break
|
143
|
+
lines.pop()
|
144
|
+
|
145
|
+
self._display = PyteDisplay(lines)
|
146
|
+
self.virtual_size = Size(
|
147
|
+
width=self.size.width - 2, # Account for possible vertical scrollbar.
|
148
|
+
height=self._display.virtual_height,
|
149
|
+
)
|
150
|
+
self.refresh()
|
151
|
+
|
152
|
+
def disconnect(self):
|
153
|
+
if self.emulator is None:
|
154
|
+
return
|
155
|
+
cb = self.emulator.callback
|
156
|
+
self.emulator.disconnect()
|
157
|
+
self.emulator = None
|
158
|
+
self.recv_queue = asyncio.Queue()
|
159
|
+
self.send_queue = asyncio.Queue()
|
160
|
+
if cb is not None:
|
161
|
+
cb()
|
162
|
+
self.recv_task.cancel()
|
163
|
+
|
164
|
+
async def recv(self):
|
165
|
+
while True:
|
166
|
+
msg = await self.recv_queue.get()
|
167
|
+
cmd = msg[0]
|
168
|
+
if cmd == 'setup':
|
169
|
+
await self.send_queue.put(
|
170
|
+
[
|
171
|
+
'set_size',
|
172
|
+
self.virtual_size.height,
|
173
|
+
self.virtual_size.width,
|
174
|
+
]
|
175
|
+
)
|
176
|
+
elif cmd == 'stdout':
|
177
|
+
chars = msg[1]
|
178
|
+
self.stream.feed(chars)
|
179
|
+
self.update_display()
|
180
|
+
elif cmd == 'disconnect':
|
181
|
+
self.disconnect()
|
182
|
+
|
183
|
+
def char_rich_style(self, char: Char) -> Style:
|
184
|
+
"""Returns a rich.Style from the pyte.Char."""
|
185
|
+
|
186
|
+
foreground = self.detect_color(char.fg)
|
187
|
+
background = self.detect_color(char.bg)
|
188
|
+
if self.default_colors == 'textual':
|
189
|
+
if background == 'default':
|
190
|
+
background = self.textual_colors['background']
|
191
|
+
if foreground == 'default':
|
192
|
+
foreground = self.textual_colors['foreground']
|
193
|
+
|
194
|
+
style: Optional[Style]
|
195
|
+
try:
|
196
|
+
style = Style(
|
197
|
+
color=foreground,
|
198
|
+
bgcolor=background,
|
199
|
+
bold=char.bold,
|
200
|
+
)
|
201
|
+
except ColorParseError as error:
|
202
|
+
textual.log.warning('color parse error:', error)
|
203
|
+
style = None
|
204
|
+
|
205
|
+
return style or Style()
|
206
|
+
|
207
|
+
def char_style_cmp(self, given: Char, other: Char) -> bool:
|
208
|
+
"""Compares two pyte.Chars and returns if these are the same.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
True if char styles are the same
|
212
|
+
False if char styles differ
|
213
|
+
"""
|
214
|
+
|
215
|
+
if (
|
216
|
+
given.fg == other.fg
|
217
|
+
and given.bg == other.bg
|
218
|
+
and given.bold == other.bold
|
219
|
+
and given.italics == other.italics
|
220
|
+
and given.underscore == other.underscore
|
221
|
+
and given.strikethrough == other.strikethrough
|
222
|
+
and given.reverse == other.reverse
|
223
|
+
and given.blink == other.blink
|
224
|
+
):
|
225
|
+
return True
|
226
|
+
|
227
|
+
return False
|
228
|
+
|
229
|
+
def char_style_default(self, char: Char) -> bool:
|
230
|
+
"""Returns True if the given char has a default style."""
|
231
|
+
|
232
|
+
if (
|
233
|
+
char.fg == 'default'
|
234
|
+
and char.bg == 'default'
|
235
|
+
and char.bold is False
|
236
|
+
and char.italics is False
|
237
|
+
and char.underscore is False
|
238
|
+
and char.strikethrough is False
|
239
|
+
and char.reverse is False
|
240
|
+
and char.blink is False
|
241
|
+
):
|
242
|
+
return True
|
243
|
+
|
244
|
+
return False
|
245
|
+
|
246
|
+
def detect_color(self, color: str) -> str:
|
247
|
+
"""Tries to detect the correct Rich-Color based on a color name.
|
248
|
+
|
249
|
+
* Returns #<color> if <color> is a hex-definition without "#"
|
250
|
+
* Fixes wrong ANSI color names.
|
251
|
+
|
252
|
+
Examples:
|
253
|
+
* htop is using "brown" => not an ANSI color
|
254
|
+
"""
|
255
|
+
|
256
|
+
if color == 'brown':
|
257
|
+
return 'yellow'
|
258
|
+
|
259
|
+
if color == 'brightblack':
|
260
|
+
# fish tabbing through recommendations
|
261
|
+
return '#808080'
|
262
|
+
if color == 'brightwhite':
|
263
|
+
return '#FFFFFF'
|
264
|
+
|
265
|
+
if re.match('[0-9a-f]{6}', color, re.IGNORECASE):
|
266
|
+
return f'#{color}'
|
267
|
+
|
268
|
+
return color
|
269
|
+
|
270
|
+
def detect_textual_colors(self) -> dict:
|
271
|
+
"""Returns the currently used colors of textual depending on dark-mode."""
|
272
|
+
|
273
|
+
if self.app.dark:
|
274
|
+
color_system: ColorSystem = DEFAULT_COLORS['dark']
|
275
|
+
else:
|
276
|
+
color_system: ColorSystem = DEFAULT_COLORS['light']
|
277
|
+
|
278
|
+
return color_system.generate()
|
279
|
+
|
280
|
+
def render_line(self, y: int) -> Strip:
|
281
|
+
scroll_x, scroll_y = self.scroll_offset
|
282
|
+
y += scroll_y
|
283
|
+
if y >= len(self._display.lines):
|
284
|
+
return Strip.blank(self.size.width)
|
285
|
+
line = self._display.lines[y]
|
286
|
+
strip = Strip(line).crop(scroll_x, scroll_x + self.size.width)
|
287
|
+
return strip
|
288
|
+
|
289
|
+
def export(self):
|
290
|
+
return LogDisplayState(screen=copy.copy(self._screen), exitcode=self.exitcode)
|
291
|
+
|
292
|
+
def load(self, state: LogDisplayState):
|
293
|
+
self.disconnect()
|
294
|
+
self._screen = state.screen
|
295
|
+
self.stream = pyte.Stream(self._screen)
|
296
|
+
self.exitcode = state.exitcode
|
297
|
+
self.update_display()
|
298
|
+
|
299
|
+
async def capture(self, argv: List[str]) -> int:
|
300
|
+
self.exitcode = None
|
301
|
+
self.recv_task = asyncio.create_task(self.recv())
|
302
|
+
|
303
|
+
loop = asyncio.get_running_loop()
|
304
|
+
send_queue = self.recv_queue
|
305
|
+
recv_queue = self.send_queue
|
306
|
+
event = asyncio.Event()
|
307
|
+
|
308
|
+
pid, fd = pty.fork()
|
309
|
+
if pid == 0: # Child
|
310
|
+
os.execvp(argv[0], argv)
|
311
|
+
|
312
|
+
pout = os.fdopen(fd, 'w+b', 0)
|
313
|
+
data: Optional[str] = None
|
314
|
+
|
315
|
+
def on_output():
|
316
|
+
nonlocal data
|
317
|
+
try:
|
318
|
+
data = pout.read(65536).decode() # Read non-blocking.
|
319
|
+
except Exception:
|
320
|
+
data = None
|
321
|
+
loop.remove_reader(pout)
|
322
|
+
event.set()
|
323
|
+
|
324
|
+
async def cleanup():
|
325
|
+
try:
|
326
|
+
loop.remove_reader(pout)
|
327
|
+
_, exitstatus = os.waitpid(pid, os.WNOHANG)
|
328
|
+
exitcode = os.waitstatus_to_exitcode(exitstatus)
|
329
|
+
self.exitcode = exitcode
|
330
|
+
except ChildProcessError:
|
331
|
+
self.exitcode = -1
|
332
|
+
await send_queue.put(['disconnect', 1])
|
333
|
+
|
334
|
+
async def communicate():
|
335
|
+
await send_queue.put(['setup', {}])
|
336
|
+
|
337
|
+
while True:
|
338
|
+
msg = await recv_queue.get()
|
339
|
+
if msg[0] == 'set_size':
|
340
|
+
winsize = struct.pack('HH', msg[1], msg[2])
|
341
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
|
342
|
+
|
343
|
+
async def send():
|
344
|
+
while True:
|
345
|
+
await event.wait()
|
346
|
+
event.clear()
|
347
|
+
if data is None:
|
348
|
+
await cleanup()
|
349
|
+
else:
|
350
|
+
await send_queue.put(['stdout', data])
|
351
|
+
|
352
|
+
async def wait():
|
353
|
+
while True:
|
354
|
+
try:
|
355
|
+
if os.waitpid(pid, os.WNOHANG) != (0, 0):
|
356
|
+
await cleanup()
|
357
|
+
except ChildProcessError:
|
358
|
+
break
|
359
|
+
await asyncio.sleep(0.5)
|
360
|
+
|
361
|
+
finish = asyncio.Event()
|
362
|
+
self.emulator = Emulator(
|
363
|
+
asyncio.create_task(communicate()),
|
364
|
+
asyncio.create_task(send()),
|
365
|
+
asyncio.create_task(wait()),
|
366
|
+
pid=pid,
|
367
|
+
callback=lambda: finish.set(),
|
368
|
+
)
|
369
|
+
loop.add_reader(pout, on_output)
|
370
|
+
await finish.wait()
|
371
|
+
assert self.exitcode is not None
|
372
|
+
return self.exitcode
|
rbx/box/ui/css/app.tcss
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
Screen {
|
2
|
+
background: $background;
|
3
|
+
color: $text;
|
4
|
+
}
|
5
|
+
|
6
|
+
rbxApp > Screen {
|
7
|
+
align: center middle;
|
8
|
+
}
|
9
|
+
|
10
|
+
SelectionList {
|
11
|
+
border-title-color: $accent;
|
12
|
+
}
|
13
|
+
|
14
|
+
Button {
|
15
|
+
width: 1fr;
|
16
|
+
background: $accent;
|
17
|
+
}
|
18
|
+
|
19
|
+
#report-grid {
|
20
|
+
layout: grid;
|
21
|
+
grid-size: 2 2;
|
22
|
+
}
|
23
|
+
|
24
|
+
DataTable {
|
25
|
+
border: solid $accent 100%;
|
26
|
+
}
|
27
|
+
|
28
|
+
DataTable > .datatable--cursor {
|
29
|
+
color: $text;
|
30
|
+
background: $accent;
|
31
|
+
}
|
32
|
+
|
33
|
+
DataTable:blur > .datatable--cursor {
|
34
|
+
color: $text;
|
35
|
+
background: transparent;
|
36
|
+
}
|
37
|
+
|
38
|
+
RunScreen {
|
39
|
+
align: center middle;
|
40
|
+
#run-settings {
|
41
|
+
border: thick $background 40%;
|
42
|
+
}
|
43
|
+
LogDisplay {
|
44
|
+
width: 1fr;
|
45
|
+
padding: 0 1;
|
46
|
+
border: solid $accent;
|
47
|
+
}
|
48
|
+
}
|
rbx/box/ui/main.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
from typing import Type
|
2
|
+
|
3
|
+
from textual.app import App, ComposeResult
|
4
|
+
from textual.containers import Center
|
5
|
+
from textual.screen import Screen
|
6
|
+
from textual.widgets import Footer, Header, OptionList
|
7
|
+
|
8
|
+
from rbx.box.ui.run import RunScreen
|
9
|
+
|
10
|
+
SCREEN_OPTIONS = [
|
11
|
+
('Run solutions against define testsets.', RunScreen),
|
12
|
+
]
|
13
|
+
|
14
|
+
|
15
|
+
class rbxApp(App):
|
16
|
+
TITLE = 'rbx'
|
17
|
+
CSS_PATH = 'css/app.tcss'
|
18
|
+
BINDINGS = [('q', 'quit', 'Quit')]
|
19
|
+
|
20
|
+
def compose(self) -> ComposeResult:
|
21
|
+
yield Header()
|
22
|
+
yield Footer()
|
23
|
+
with Center(id='main'):
|
24
|
+
yield OptionList(*(opt[0] for opt in SCREEN_OPTIONS))
|
25
|
+
|
26
|
+
def on_mount(self):
|
27
|
+
self.query_one(OptionList).border_title = 'Select a flow'
|
28
|
+
|
29
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected):
|
30
|
+
self.show_screen(SCREEN_OPTIONS[event.option_index][1])
|
31
|
+
|
32
|
+
def show_screen(self, screen_cls: Type[Screen]):
|
33
|
+
self.push_screen(screen_cls())
|
34
|
+
|
35
|
+
|
36
|
+
def start():
|
37
|
+
app = rbxApp()
|
38
|
+
app.run()
|
rbx/box/ui/run.py
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
import pathlib
|
2
|
+
from typing import Optional, Set
|
3
|
+
|
4
|
+
import textual
|
5
|
+
from rich.text import Text
|
6
|
+
from textual.app import ComposeResult
|
7
|
+
from textual.containers import Center, Container, Grid
|
8
|
+
from textual.coordinate import Coordinate
|
9
|
+
from textual.screen import Screen
|
10
|
+
from textual.widgets import Button, DataTable, Footer, Header, SelectionList
|
11
|
+
from textual.widgets.selection_list import Selection
|
12
|
+
|
13
|
+
from rbx import console
|
14
|
+
from rbx.box import package
|
15
|
+
from rbx.box.schema import Solution
|
16
|
+
from rbx.box.solutions import (
|
17
|
+
EvaluationItem,
|
18
|
+
SolutionReportSkeleton,
|
19
|
+
get_evals_formatted_time,
|
20
|
+
get_testcase_markup_verdict,
|
21
|
+
run_solutions,
|
22
|
+
)
|
23
|
+
from rbx.box.ui.captured_log import LogDisplay, LogDisplayState
|
24
|
+
|
25
|
+
|
26
|
+
def _build_solution_selection_label(sol: Solution) -> Text:
|
27
|
+
main = package.get_main_solution()
|
28
|
+
outcome = sol.outcome if main is None or main.path != sol.path else 'MAIN'
|
29
|
+
|
30
|
+
style = sol.outcome.style()
|
31
|
+
text = Text(f'{sol.path}')
|
32
|
+
text.append(f' {outcome}', style=style)
|
33
|
+
return text
|
34
|
+
|
35
|
+
|
36
|
+
class SolutionReportScreen(Screen):
|
37
|
+
skeleton: SolutionReportSkeleton
|
38
|
+
|
39
|
+
BINDINGS = [('q', 'app.pop_screen', 'Quit')]
|
40
|
+
|
41
|
+
def __init__(
|
42
|
+
self,
|
43
|
+
skeleton: SolutionReportSkeleton,
|
44
|
+
log_display_state: Optional[LogDisplayState] = None,
|
45
|
+
):
|
46
|
+
super().__init__()
|
47
|
+
self.skeleton = skeleton
|
48
|
+
self.log_display_state = log_display_state
|
49
|
+
|
50
|
+
def compose(self) -> ComposeResult:
|
51
|
+
textual.log(self.skeleton)
|
52
|
+
yield Header()
|
53
|
+
yield Footer()
|
54
|
+
with Grid(id='report-grid'):
|
55
|
+
for _ in self.skeleton.solutions:
|
56
|
+
yield DataTable(
|
57
|
+
cursor_type='row', cursor_foreground_priority='renderable'
|
58
|
+
)
|
59
|
+
if self.log_display_state is not None:
|
60
|
+
yield LogDisplay()
|
61
|
+
|
62
|
+
def on_mount(self):
|
63
|
+
self.query_one(
|
64
|
+
'#build-output', Container
|
65
|
+
).border_title = 'Test generation and validation'
|
66
|
+
for solution, table in zip(
|
67
|
+
self.skeleton.solutions,
|
68
|
+
self.query(DataTable),
|
69
|
+
):
|
70
|
+
table.border_title = str(solution.path)
|
71
|
+
table.border_subtitle = _build_solution_selection_label(solution)
|
72
|
+
table.add_columns('group', 'test', '?', 'time')
|
73
|
+
|
74
|
+
for group in self.skeleton.groups:
|
75
|
+
for i, tc in enumerate(group.testcases):
|
76
|
+
table.add_row(group.name, i, '', '', key=str(tc.inputPath))
|
77
|
+
|
78
|
+
self.query_one(DataTable).focus()
|
79
|
+
|
80
|
+
if self.log_display_state is not None:
|
81
|
+
self.query_one(LogDisplay).load(self.log_display_state)
|
82
|
+
|
83
|
+
def _find_solution_index_in_skeleton(self, sol: Solution) -> int:
|
84
|
+
for i, solution in enumerate(self.skeleton.solutions):
|
85
|
+
if solution.path == sol.path:
|
86
|
+
return i
|
87
|
+
raise
|
88
|
+
|
89
|
+
def process(self, item: EvaluationItem):
|
90
|
+
textual.log(item)
|
91
|
+
pkg = package.find_problem_package_or_die()
|
92
|
+
sol_idx_in_skeleton = self._find_solution_index_in_skeleton(
|
93
|
+
pkg.solutions[item.solution_index]
|
94
|
+
)
|
95
|
+
group = self.skeleton.find_group_skeleton(item.group_name)
|
96
|
+
assert group is not None
|
97
|
+
tc = group.testcases[item.testcase_index]
|
98
|
+
|
99
|
+
textual.log(len(list(self.query(DataTable))), sol_idx_in_skeleton)
|
100
|
+
table = self.query(DataTable)[sol_idx_in_skeleton]
|
101
|
+
row_idx = table.get_row_index(str(tc.inputPath))
|
102
|
+
|
103
|
+
table.update_cell_at(
|
104
|
+
Coordinate(row=row_idx, column=2),
|
105
|
+
get_testcase_markup_verdict(item.eval),
|
106
|
+
update_width=True,
|
107
|
+
)
|
108
|
+
table.update_cell_at(
|
109
|
+
Coordinate(row=row_idx, column=3),
|
110
|
+
get_evals_formatted_time([item.eval]),
|
111
|
+
update_width=True,
|
112
|
+
)
|
113
|
+
|
114
|
+
|
115
|
+
class RunScreen(Screen):
|
116
|
+
def compose(self) -> ComposeResult:
|
117
|
+
yield Header()
|
118
|
+
yield Footer()
|
119
|
+
with Center(id='run-settings'):
|
120
|
+
pkg = package.find_problem_package_or_die()
|
121
|
+
solutions = package.get_solutions()
|
122
|
+
yield SelectionList[pathlib.Path](
|
123
|
+
*(
|
124
|
+
Selection(_build_solution_selection_label(sol), sol.path, True)
|
125
|
+
for sol in solutions
|
126
|
+
),
|
127
|
+
id='run-sols',
|
128
|
+
)
|
129
|
+
yield SelectionList[str](
|
130
|
+
*(
|
131
|
+
Selection(testgroup.name, testgroup.name, initial_state=True)
|
132
|
+
for testgroup in pkg.testcases
|
133
|
+
),
|
134
|
+
id='run-testgroups',
|
135
|
+
)
|
136
|
+
yield SelectionList[str](
|
137
|
+
Selection(
|
138
|
+
'Generate expected outputs and run checker',
|
139
|
+
'check',
|
140
|
+
initial_state=True,
|
141
|
+
),
|
142
|
+
id='run-config',
|
143
|
+
)
|
144
|
+
yield Button('Run')
|
145
|
+
yield LogDisplay()
|
146
|
+
|
147
|
+
def on_mount(self):
|
148
|
+
sols = self.query_one('#run-sols', SelectionList)
|
149
|
+
sols.border_title = 'Select solutions to run'
|
150
|
+
sols.focus()
|
151
|
+
|
152
|
+
testgroups = self.query_one('#run-testgroups', SelectionList)
|
153
|
+
testgroups.border_title = 'Select which testgroups to execute'
|
154
|
+
|
155
|
+
config = self.query_one('#run-config', SelectionList)
|
156
|
+
config.border_title = 'Configure the execution'
|
157
|
+
|
158
|
+
def on_screen_resume(self):
|
159
|
+
self.query_one('#run-sols', SelectionList).focus()
|
160
|
+
|
161
|
+
def on_button_pressed(self, _: Button.Pressed):
|
162
|
+
self.action_run()
|
163
|
+
|
164
|
+
@textual.work(thread=True)
|
165
|
+
def _run_solutions(self, tracked_solutions: Set[str], check: bool):
|
166
|
+
main_solution = package.get_main_solution()
|
167
|
+
if check and main_solution is None:
|
168
|
+
console.console.print(
|
169
|
+
'[warning]No main solution found, running without checkers.[/warning]'
|
170
|
+
)
|
171
|
+
check = False
|
172
|
+
|
173
|
+
async def build():
|
174
|
+
return await self.query_one(LogDisplay).capture(['rbx', 'build'])
|
175
|
+
|
176
|
+
exitcode = self.app.call_from_thread(build)
|
177
|
+
|
178
|
+
if exitcode != 0:
|
179
|
+
textual.log('early quit')
|
180
|
+
return
|
181
|
+
|
182
|
+
res = run_solutions(tracked_solutions=tracked_solutions, check=check)
|
183
|
+
|
184
|
+
async def mount_report_widget() -> SolutionReportScreen:
|
185
|
+
# log_display_state = self.query_one(LogDisplay).export()
|
186
|
+
log_display_state = None
|
187
|
+
await self.app.push_screen(
|
188
|
+
screen := SolutionReportScreen(
|
189
|
+
res.skeleton, log_display_state=log_display_state
|
190
|
+
)
|
191
|
+
)
|
192
|
+
return screen
|
193
|
+
|
194
|
+
new_screen = self.app.call_from_thread(mount_report_widget)
|
195
|
+
|
196
|
+
def process(item: EvaluationItem):
|
197
|
+
new_screen.process(item)
|
198
|
+
|
199
|
+
for item in res.items:
|
200
|
+
self.app.call_from_thread(process, item)
|
201
|
+
|
202
|
+
def action_run(self):
|
203
|
+
sols = self.query_one('#run-sols', SelectionList)
|
204
|
+
config = self.query_one('#run-config', SelectionList)
|
205
|
+
|
206
|
+
tracked_solutions = set(str(sol) for sol in sols.selected)
|
207
|
+
check = 'check' in config.selected
|
208
|
+
|
209
|
+
self._run_solutions(tracked_solutions, check)
|