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,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
@@ -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)