rbx.cp 0.5.54__py3-none-any.whl → 0.5.55__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/box/checkers.py +11 -1
- rbx/box/cli.py +8 -0
- rbx/box/contest/schema.py +53 -4
- rbx/box/naming.py +20 -5
- rbx/box/packaging/boca/upload.py +247 -0
- rbx/box/packaging/main.py +13 -1
- rbx/box/solutions.py +12 -1
- rbx/box/tasks.py +4 -2
- rbx/box/testcase_extractors.py +3 -0
- rbx/box/ui/captured_log.py +13 -8
- rbx/box/ui/css/app.tcss +47 -8
- rbx/box/ui/main.py +5 -1
- rbx/box/ui/screens/__init__.py +0 -0
- rbx/box/ui/screens/build.py +6 -0
- rbx/box/ui/screens/command.py +35 -0
- rbx/box/ui/{run.py → screens/run.py} +10 -38
- rbx/box/ui/screens/run_explorer.py +5 -0
- rbx/box/ui/screens/test_explorer.py +100 -0
- rbx/box/ui/widgets/file_log.py +63 -0
- rbx/box/ui/widgets/rich_log_box.py +5 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +5 -1
- rbx/grading/judge/sandboxes/timeit.py +2 -1
- rbx/resources/packagers/boca/interactive/c +8 -1
- rbx/resources/packagers/boca/interactive/cc +8 -1
- rbx/resources/packagers/boca/interactive/cpp +8 -1
- rbx/resources/packagers/boca/interactive/java +8 -1
- rbx/resources/packagers/boca/interactive/kt +8 -1
- rbx/resources/packagers/boca/interactive/py2 +8 -1
- rbx/resources/packagers/boca/interactive/py3 +8 -1
- {rbx_cp-0.5.54.dist-info → rbx_cp-0.5.55.dist-info}/METADATA +7 -2
- {rbx_cp-0.5.54.dist-info → rbx_cp-0.5.55.dist-info}/RECORD +34 -26
- {rbx_cp-0.5.54.dist-info → rbx_cp-0.5.55.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.54.dist-info → rbx_cp-0.5.55.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.54.dist-info → rbx_cp-0.5.55.dist-info}/entry_points.txt +0 -0
rbx/box/checkers.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import pathlib
|
2
2
|
import signal
|
3
|
-
from typing import Optional
|
3
|
+
from typing import List, Optional
|
4
4
|
|
5
5
|
import typer
|
6
6
|
|
@@ -53,6 +53,12 @@ def compile_interactor(progress: Optional[StatusProgress] = None) -> str:
|
|
53
53
|
return digest
|
54
54
|
|
55
55
|
|
56
|
+
def _any_failed(logs: List[Optional[RunLog]]) -> bool:
|
57
|
+
return any(
|
58
|
+
log is None or log.exitstatus == SandboxBase.EXIT_SANDBOX_ERROR for log in logs
|
59
|
+
)
|
60
|
+
|
61
|
+
|
56
62
|
def _check_pre_output(run_log: Optional[RunLog]) -> CheckerResult:
|
57
63
|
pkg = package.find_problem_package_or_die()
|
58
64
|
|
@@ -283,6 +289,10 @@ async def check_communication(
|
|
283
289
|
# No relevant error was found.
|
284
290
|
return None
|
285
291
|
|
292
|
+
# 0. If any of the sandboxes failed, we should return an error.
|
293
|
+
if _any_failed([run_log, interactor_run_log]):
|
294
|
+
return CheckerResult(outcome=Outcome.INTERNAL_ERROR)
|
295
|
+
|
286
296
|
# 1. If the solution received SIGPIPE or was terminated, it means the
|
287
297
|
# interactor exited before it. Thus, check the interactor, as it might have
|
288
298
|
# returned a checker verdict.
|
rbx/box/cli.py
CHANGED
@@ -130,6 +130,14 @@ def ui():
|
|
130
130
|
ui_pkg.start()
|
131
131
|
|
132
132
|
|
133
|
+
@app.command('serve', hidden=True)
|
134
|
+
def serve():
|
135
|
+
from textual_serve.server import Server
|
136
|
+
|
137
|
+
server = Server('rbx ui', port=8081)
|
138
|
+
server.serve()
|
139
|
+
|
140
|
+
|
133
141
|
@app.command(
|
134
142
|
'edit, e',
|
135
143
|
rich_help_panel='Configuration',
|
rbx/box/contest/schema.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import pathlib
|
2
2
|
from typing import Dict, List, Optional
|
3
3
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, Field
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
5
5
|
|
6
6
|
from rbx.box.schema import NameField, Primitive, expand_var
|
7
7
|
from rbx.box.statements.schema import (
|
@@ -121,11 +121,60 @@ If not specified, will expect the problem to be in ./{short_name}/ folder.""",
|
|
121
121
|
|
122
122
|
color: Optional[str] = Field(
|
123
123
|
default=None,
|
124
|
-
description="""
|
125
|
-
|
126
|
-
|
124
|
+
description="""
|
125
|
+
Color that represents this problem in the contest.
|
126
|
+
|
127
|
+
Can be a hex color (#abcdef or #abc format), or a color name among available X11 colors.
|
128
|
+
|
129
|
+
See https://en.wikipedia.org/wiki/X11_color_names for the list of supported color names.
|
130
|
+
""",
|
127
131
|
)
|
128
132
|
|
133
|
+
colorName: Optional[str] = Field(
|
134
|
+
default=None,
|
135
|
+
description="""
|
136
|
+
A custom color name for the color provided by this problem.
|
137
|
+
|
138
|
+
If not provided, will try to infer a color name from the color provided.
|
139
|
+
""",
|
140
|
+
pattern=r'^[a-zA-Z]+$',
|
141
|
+
)
|
142
|
+
|
143
|
+
@model_validator(mode='after')
|
144
|
+
def check_color(self):
|
145
|
+
from colour import Color
|
146
|
+
|
147
|
+
if self.color is None:
|
148
|
+
return self
|
149
|
+
|
150
|
+
Color(self.color)
|
151
|
+
return self
|
152
|
+
|
153
|
+
@property
|
154
|
+
def hex_color(self) -> Optional[str]:
|
155
|
+
from colour import Color
|
156
|
+
|
157
|
+
if self.color is None:
|
158
|
+
return None
|
159
|
+
|
160
|
+
return Color(self.color).hex_l
|
161
|
+
|
162
|
+
@property
|
163
|
+
def color_name(self) -> Optional[str]:
|
164
|
+
if self.colorName is not None:
|
165
|
+
return self.colorName
|
166
|
+
|
167
|
+
if self.color is None:
|
168
|
+
return None
|
169
|
+
|
170
|
+
from colour import Color
|
171
|
+
|
172
|
+
color = Color(self.color)
|
173
|
+
web_color = color.web
|
174
|
+
if web_color.startswith('#'):
|
175
|
+
return 'unknown'
|
176
|
+
return web_color
|
177
|
+
|
129
178
|
def get_path(self) -> pathlib.Path:
|
130
179
|
return self.path or pathlib.Path(self.short_name)
|
131
180
|
|
rbx/box/naming.py
CHANGED
@@ -1,27 +1,42 @@
|
|
1
|
-
from typing import Optional
|
1
|
+
from typing import Optional, Tuple
|
2
2
|
|
3
3
|
from rbx.box import package
|
4
4
|
from rbx.box.contest import contest_package
|
5
|
+
from rbx.box.contest.schema import ContestProblem
|
5
6
|
|
6
7
|
|
7
|
-
def
|
8
|
+
def get_problem_entry_in_contest() -> Optional[Tuple[int, ContestProblem]]:
|
8
9
|
contest = contest_package.find_contest_package()
|
9
10
|
if contest is None:
|
10
11
|
return None
|
11
12
|
problem_path = package.find_problem()
|
12
13
|
contest_path = contest_package.find_contest()
|
13
14
|
|
14
|
-
for problem in contest.problems:
|
15
|
+
for i, problem in enumerate(contest.problems):
|
15
16
|
if problem.path is None:
|
16
17
|
continue
|
17
18
|
if (problem_path / 'problem.rbx.yml').samefile(
|
18
19
|
contest_path / problem.path / 'problem.rbx.yml'
|
19
20
|
):
|
20
|
-
return problem
|
21
|
-
|
21
|
+
return i, problem
|
22
22
|
return None
|
23
23
|
|
24
24
|
|
25
|
+
def get_problem_shortname() -> Optional[str]:
|
26
|
+
entry = get_problem_entry_in_contest()
|
27
|
+
if entry is None:
|
28
|
+
return None
|
29
|
+
_, problem = entry
|
30
|
+
return problem.short_name
|
31
|
+
|
32
|
+
|
33
|
+
def get_problem_index() -> Optional[int]:
|
34
|
+
entry = get_problem_entry_in_contest()
|
35
|
+
if entry is None:
|
36
|
+
return None
|
37
|
+
return entry[0]
|
38
|
+
|
39
|
+
|
25
40
|
def get_problem_name_with_contest_info() -> str:
|
26
41
|
problem = package.find_problem_package_or_die()
|
27
42
|
contest = contest_package.find_contest_package()
|
@@ -0,0 +1,247 @@
|
|
1
|
+
import datetime
|
2
|
+
import hashlib
|
3
|
+
import os
|
4
|
+
import pathlib
|
5
|
+
import re
|
6
|
+
import typing
|
7
|
+
from typing import Any, NoReturn, Optional, Tuple
|
8
|
+
|
9
|
+
import dateparser
|
10
|
+
import mechanize
|
11
|
+
import typer
|
12
|
+
from bs4 import BeautifulSoup
|
13
|
+
|
14
|
+
from rbx import console
|
15
|
+
from rbx.box import naming
|
16
|
+
|
17
|
+
ALERT_REGEX = re.compile(r'\<script[^\>]*\>\s*alert\(\'([^\']+)\'\);?\s*\<\/script\>')
|
18
|
+
UPLOAD_LOG_REGEX = re.compile(r'Problem (\d+) \([^\)]+\) updated')
|
19
|
+
|
20
|
+
|
21
|
+
def _parse_env_var(var: str, override: Optional[str]) -> str:
|
22
|
+
if override is not None:
|
23
|
+
return override
|
24
|
+
value = os.environ.get(var)
|
25
|
+
if value is None:
|
26
|
+
console.console.print(
|
27
|
+
f'[error][item]{var}[/item] is not set. Set it as an environment variable.[/error]'
|
28
|
+
)
|
29
|
+
raise typer.Exit(1)
|
30
|
+
return value
|
31
|
+
|
32
|
+
|
33
|
+
class BocaUploader:
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
base_url: Optional[str] = None,
|
37
|
+
username: Optional[str] = None,
|
38
|
+
password: Optional[str] = None,
|
39
|
+
):
|
40
|
+
self.base_url = _parse_env_var('BOCA_BASE_URL', base_url)
|
41
|
+
self.username = _parse_env_var('BOCA_USERNAME', username)
|
42
|
+
self.password = _parse_env_var('BOCA_PASSWORD', password)
|
43
|
+
|
44
|
+
self.br = mechanize.Browser()
|
45
|
+
self.br.set_handle_robots(False)
|
46
|
+
self.br.addheaders = [ # type: ignore
|
47
|
+
(
|
48
|
+
'User-agent',
|
49
|
+
'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1',
|
50
|
+
)
|
51
|
+
]
|
52
|
+
|
53
|
+
def error(self, message: str) -> NoReturn:
|
54
|
+
console.console.print(
|
55
|
+
f'[error]{message} (at [item]{self.base_url}[/item])[/error]',
|
56
|
+
)
|
57
|
+
raise typer.Exit(1)
|
58
|
+
|
59
|
+
def raw_error(self, message: str) -> NoReturn:
|
60
|
+
console.console.print(f'[error]{message}[/error]')
|
61
|
+
raise typer.Exit(1)
|
62
|
+
|
63
|
+
def log_response_alert(self, response: Any, message: str) -> Tuple[Any, str]:
|
64
|
+
if response is None:
|
65
|
+
self.raw_error(
|
66
|
+
f'{message} ([item]{self.base_url}[/item]):\nNo response received.'
|
67
|
+
)
|
68
|
+
html = response.read().decode()
|
69
|
+
alert = ALERT_REGEX.search(html)
|
70
|
+
if alert:
|
71
|
+
self.raw_error(
|
72
|
+
f'{message} ([item]{self.base_url}[/item]):\n{alert.group(1)}'
|
73
|
+
)
|
74
|
+
return response, html
|
75
|
+
|
76
|
+
def check_logs_for_update(self, problem_id: int) -> bool:
|
77
|
+
_, html = self.open(
|
78
|
+
f'{self.base_url}/admin/log.php',
|
79
|
+
error_msg='Error while checking whether package upload was successful',
|
80
|
+
)
|
81
|
+
|
82
|
+
soup = BeautifulSoup(html, 'html.parser')
|
83
|
+
table = soup.select_one('table:nth-of-type(3)')
|
84
|
+
if table is None:
|
85
|
+
self.raw_error(
|
86
|
+
'Error while checking whether package upload was successful:\nNo logs table found.'
|
87
|
+
)
|
88
|
+
rows = table.select('tr:not(:first-child)')
|
89
|
+
for row in rows:
|
90
|
+
# Check whether the log line is recent.
|
91
|
+
date_cell = row.select('td:nth-of-type(5)')
|
92
|
+
if date_cell is None:
|
93
|
+
continue
|
94
|
+
date = date_cell[0].text.strip()
|
95
|
+
|
96
|
+
parsed_date = dateparser.parse(date)
|
97
|
+
if parsed_date is None:
|
98
|
+
console.console.print(
|
99
|
+
f'Error while checking whether package upload was successful:\nCould not parse date [item]{date}[/item].'
|
100
|
+
)
|
101
|
+
raise typer.Exit(1)
|
102
|
+
if parsed_date < datetime.datetime.now(
|
103
|
+
datetime.timezone.utc
|
104
|
+
) - datetime.timedelta(minutes=5):
|
105
|
+
continue
|
106
|
+
|
107
|
+
# Check if the log line contains the problem id.
|
108
|
+
log_cell = row.select('td:nth-of-type(6)')
|
109
|
+
if log_cell is None:
|
110
|
+
continue
|
111
|
+
log_line = log_cell[0].text.strip()
|
112
|
+
match = UPLOAD_LOG_REGEX.match(log_line)
|
113
|
+
if match is None:
|
114
|
+
continue
|
115
|
+
found_id = int(match.group(1))
|
116
|
+
if found_id == problem_id:
|
117
|
+
return True
|
118
|
+
return False
|
119
|
+
|
120
|
+
def check_submit(self, response: Any, problem_id: int) -> bool:
|
121
|
+
if response is None:
|
122
|
+
self.raw_error(
|
123
|
+
'Error while submitting problem to BOCA website:\nNo response received.'
|
124
|
+
)
|
125
|
+
html = response.read().decode()
|
126
|
+
alert = ALERT_REGEX.search(html)
|
127
|
+
if alert:
|
128
|
+
msg = alert.group(1)
|
129
|
+
if 'Violation' in msg:
|
130
|
+
return False
|
131
|
+
self.raw_error(
|
132
|
+
f'Error while submitting problem to BOCA website:\n{alert.group(1)}'
|
133
|
+
)
|
134
|
+
return self.check_logs_for_update(problem_id)
|
135
|
+
|
136
|
+
def open(self, url: str, error_msg: Optional[str] = None):
|
137
|
+
if error_msg is None:
|
138
|
+
error_msg = f'Error while opening [item]{url}[/item]'
|
139
|
+
response = self.br.open(url)
|
140
|
+
return self.log_response_alert(response, error_msg)
|
141
|
+
|
142
|
+
def login(self):
|
143
|
+
_, html = self.open(
|
144
|
+
f'{self.base_url}', error_msg='Error while opening BOCA login page'
|
145
|
+
)
|
146
|
+
|
147
|
+
needle = "js_myhash(document.form1.password.value)+'"
|
148
|
+
start = html.index(needle)
|
149
|
+
start_salt = start + len(needle)
|
150
|
+
end_salt = html.index("'", start_salt)
|
151
|
+
salt = html[start_salt:end_salt]
|
152
|
+
console.console.print(f'Using salt [item]{salt}[/item]')
|
153
|
+
|
154
|
+
pwd_hash = hashlib.sha256(self.password.encode()).hexdigest()
|
155
|
+
pwd_hash = hashlib.sha256((pwd_hash + salt).encode()).hexdigest()
|
156
|
+
|
157
|
+
login_url = f'{self.base_url}?name={self.username}&password={pwd_hash}'
|
158
|
+
self.open(login_url, error_msg='Error while logging in to BOCA')
|
159
|
+
|
160
|
+
def upload(self, file: pathlib.Path) -> bool:
|
161
|
+
self.open(
|
162
|
+
f'{self.base_url}/admin/problem.php',
|
163
|
+
error_msg='Error while opening BOCA problem upload page',
|
164
|
+
)
|
165
|
+
try:
|
166
|
+
form = self.br.select_form(name='form1')
|
167
|
+
except mechanize.FormNotFoundError:
|
168
|
+
self.error(
|
169
|
+
'Problem upload form not found in BOCA website. This might happen when the login failed.'
|
170
|
+
)
|
171
|
+
|
172
|
+
form = typing.cast(mechanize.HTMLForm, self.br.form)
|
173
|
+
form.set_all_readonly(False)
|
174
|
+
|
175
|
+
problem_index = naming.get_problem_index()
|
176
|
+
if problem_index is None:
|
177
|
+
console.console.print(
|
178
|
+
'It seems this problem is not part of a contest. Cannot upload it to BOCA.'
|
179
|
+
)
|
180
|
+
raise typer.Exit(1)
|
181
|
+
|
182
|
+
problem_shortname = naming.get_problem_shortname()
|
183
|
+
assert problem_shortname is not None
|
184
|
+
entry = naming.get_problem_entry_in_contest()
|
185
|
+
assert entry is not None
|
186
|
+
_, problem_entry = entry
|
187
|
+
|
188
|
+
hex_color = problem_entry.hex_color
|
189
|
+
if hex_color is None:
|
190
|
+
form['colorname'] = 'black'
|
191
|
+
form['color'] = '000000'
|
192
|
+
else:
|
193
|
+
assert problem_entry.color_name is not None
|
194
|
+
form['colorname'] = problem_entry.color_name.lower()
|
195
|
+
form['color'] = hex_color[1:]
|
196
|
+
|
197
|
+
form['problemnumber'] = f'{problem_index + 1}'
|
198
|
+
form['problemname'] = problem_shortname
|
199
|
+
form['confirmation'] = 'confirm'
|
200
|
+
form['autojudge_new_sel'] = ['all']
|
201
|
+
form['Submit3'] = 'Send'
|
202
|
+
|
203
|
+
with file.open('rb') as f:
|
204
|
+
form.add_file(
|
205
|
+
f,
|
206
|
+
filename=file.name,
|
207
|
+
name='probleminput',
|
208
|
+
content_type='application/zip',
|
209
|
+
)
|
210
|
+
response = self.br.submit()
|
211
|
+
|
212
|
+
return self.check_submit(response, problem_index + 1)
|
213
|
+
|
214
|
+
def login_and_upload(self, file: pathlib.Path):
|
215
|
+
RETRIES = 3
|
216
|
+
|
217
|
+
tries = 0
|
218
|
+
ok = False
|
219
|
+
while tries < RETRIES:
|
220
|
+
tries += 1
|
221
|
+
|
222
|
+
console.console.print('Logging in to BOCA...')
|
223
|
+
self.login()
|
224
|
+
console.console.print('Uploading problem to BOCA...')
|
225
|
+
if not self.upload(file):
|
226
|
+
console.console.print(
|
227
|
+
f'[warning]Potentially transient error while uploading problem to BOCA. Retrying ({tries}/{RETRIES})...[/warning]'
|
228
|
+
)
|
229
|
+
continue
|
230
|
+
|
231
|
+
ok = True
|
232
|
+
console.console.print(
|
233
|
+
'[success]Problem sent to BOCA. [item]rbx[/item] cannot determine the upload succeeded, check the website to be sure.[/success]'
|
234
|
+
)
|
235
|
+
break
|
236
|
+
|
237
|
+
if not ok:
|
238
|
+
console.console.print(
|
239
|
+
'[error]Persistent error while uploading problem to BOCA website.[/error]'
|
240
|
+
)
|
241
|
+
console.console.print(
|
242
|
+
'[warning]This might be caused by PHP max upload size limit (which usually defaults to 2MBF).[/warning]'
|
243
|
+
)
|
244
|
+
console.console.print(
|
245
|
+
'[warning]Check [item]https://www.php.net/manual/en/ini.core.php#ini.sect.file-uploads[/item] for more information.[/warning]'
|
246
|
+
)
|
247
|
+
raise typer.Exit(1)
|
rbx/box/packaging/main.py
CHANGED
@@ -93,10 +93,22 @@ async def polygon(
|
|
93
93
|
@syncer.sync
|
94
94
|
async def boca(
|
95
95
|
verification: environment.VerificationParam,
|
96
|
+
upload: bool = typer.Option(
|
97
|
+
False,
|
98
|
+
'--upload',
|
99
|
+
'-u',
|
100
|
+
help='If set, will upload the package to BOCA.',
|
101
|
+
),
|
96
102
|
):
|
97
103
|
from rbx.box.packaging.boca.packager import BocaPackager
|
98
104
|
|
99
|
-
await run_packager(BocaPackager, verification=verification)
|
105
|
+
result_path = await run_packager(BocaPackager, verification=verification)
|
106
|
+
|
107
|
+
if upload:
|
108
|
+
from rbx.box.packaging.boca.upload import BocaUploader
|
109
|
+
|
110
|
+
uploader = BocaUploader('http://137.184.1.39/boca', 'admin', 'boca')
|
111
|
+
uploader.login_and_upload(result_path)
|
100
112
|
|
101
113
|
|
102
114
|
@app.command('moj', help='Build a package for MOJ.')
|
rbx/box/solutions.py
CHANGED
@@ -77,6 +77,7 @@ class GroupSkeleton(BaseModel):
|
|
77
77
|
|
78
78
|
class SolutionReportSkeleton(BaseModel):
|
79
79
|
solutions: List[Solution]
|
80
|
+
entries: List[TestcaseEntry]
|
80
81
|
groups: List[GroupSkeleton]
|
81
82
|
limits: Dict[str, Limits]
|
82
83
|
|
@@ -245,10 +246,16 @@ def _get_report_skeleton(
|
|
245
246
|
for group in pkg.testcases:
|
246
247
|
testcases = find_built_testcases(group)
|
247
248
|
groups.append(GroupSkeleton(name=group.name, testcases=testcases))
|
249
|
+
entries = [
|
250
|
+
TestcaseEntry(group=group.name, index=i)
|
251
|
+
for group in groups
|
252
|
+
for i in range(len(group.testcases))
|
253
|
+
]
|
248
254
|
return SolutionReportSkeleton(
|
249
255
|
solutions=solutions,
|
250
256
|
groups=groups,
|
251
257
|
limits=limits,
|
258
|
+
entries=entries,
|
252
259
|
)
|
253
260
|
|
254
261
|
|
@@ -349,7 +356,7 @@ def run_solutions(
|
|
349
356
|
timelimit_override: Optional[int] = None,
|
350
357
|
sanitized: bool = False,
|
351
358
|
) -> RunSolutionResult:
|
352
|
-
|
359
|
+
result = RunSolutionResult(
|
353
360
|
skeleton=_get_report_skeleton(
|
354
361
|
tracked_solutions,
|
355
362
|
verification=verification,
|
@@ -364,6 +371,10 @@ def run_solutions(
|
|
364
371
|
sanitized=sanitized,
|
365
372
|
),
|
366
373
|
)
|
374
|
+
skeleton_file = package.get_problem_runs_dir() / 'skeleton.yml'
|
375
|
+
skeleton_file.parent.mkdir(parents=True, exist_ok=True)
|
376
|
+
skeleton_file.write_text(utils.model_to_yaml(result.skeleton))
|
377
|
+
return result
|
367
378
|
|
368
379
|
|
369
380
|
async def _generate_testcase_interactively(
|
rbx/box/tasks.py
CHANGED
@@ -272,11 +272,13 @@ async def _run_communication_solution_on_testcase(
|
|
272
272
|
|
273
273
|
log_path.write_text(model_to_yaml(eval))
|
274
274
|
|
275
|
+
interactor_log_path = output_path.with_suffix('.int.log')
|
276
|
+
interactor_log_path.unlink(missing_ok=True)
|
275
277
|
if interactor_run_log is not None:
|
276
|
-
interactor_log_path = output_path.with_suffix('.int.log')
|
277
278
|
interactor_log_path.write_text(model_to_yaml(interactor_run_log))
|
279
|
+
solution_log_path = output_path.with_suffix('.sol.log')
|
280
|
+
solution_log_path.unlink(missing_ok=True)
|
278
281
|
if run_log is not None:
|
279
|
-
solution_log_path = output_path.with_suffix('.sol.log')
|
280
282
|
solution_log_path.write_text(model_to_yaml(run_log))
|
281
283
|
return eval
|
282
284
|
|
rbx/box/testcase_extractors.py
CHANGED
rbx/box/ui/captured_log.py
CHANGED
@@ -18,8 +18,6 @@ from rich.segment import Segment
|
|
18
18
|
from rich.style import Style
|
19
19
|
from rich.text import Text
|
20
20
|
from textual import events
|
21
|
-
from textual.app import DEFAULT_COLORS
|
22
|
-
from textual.design import ColorSystem
|
23
21
|
from textual.geometry import Size
|
24
22
|
from textual.scroll_view import ScrollView
|
25
23
|
from textual.strip import Strip
|
@@ -95,6 +93,13 @@ class LogDisplay(ScrollView, can_focus=True):
|
|
95
93
|
self.send_queue = asyncio.Queue()
|
96
94
|
self.exitcode = None
|
97
95
|
|
96
|
+
def _resize(self):
|
97
|
+
self.virtual_size = Size(
|
98
|
+
width=self.size.width - 2, # Account for scroll bar.
|
99
|
+
height=self.virtual_size.height,
|
100
|
+
)
|
101
|
+
self._screen.resize(self._max_lines, self.virtual_size.width)
|
102
|
+
|
98
103
|
async def on_resize(self, _event: events.Resize):
|
99
104
|
if self.emulator is None:
|
100
105
|
return
|
@@ -161,6 +166,9 @@ class LogDisplay(ScrollView, can_focus=True):
|
|
161
166
|
cb()
|
162
167
|
self.recv_task.cancel()
|
163
168
|
|
169
|
+
def on_unmount(self):
|
170
|
+
self.disconnect()
|
171
|
+
|
164
172
|
async def recv(self):
|
165
173
|
while True:
|
166
174
|
msg = await self.recv_queue.get()
|
@@ -270,12 +278,7 @@ class LogDisplay(ScrollView, can_focus=True):
|
|
270
278
|
def detect_textual_colors(self) -> dict:
|
271
279
|
"""Returns the currently used colors of textual depending on dark-mode."""
|
272
280
|
|
273
|
-
|
274
|
-
color_system: ColorSystem = DEFAULT_COLORS['dark']
|
275
|
-
else:
|
276
|
-
color_system: ColorSystem = DEFAULT_COLORS['light']
|
277
|
-
|
278
|
-
return color_system.generate()
|
281
|
+
return self.app.current_theme.to_color_system().generate()
|
279
282
|
|
280
283
|
def render_line(self, y: int) -> Strip:
|
281
284
|
scroll_x, scroll_y = self.scroll_offset
|
@@ -312,6 +315,8 @@ class LogDisplay(ScrollView, can_focus=True):
|
|
312
315
|
pout = os.fdopen(fd, 'w+b', 0)
|
313
316
|
data: Optional[str] = None
|
314
317
|
|
318
|
+
self._resize()
|
319
|
+
|
315
320
|
def on_output():
|
316
321
|
nonlocal data
|
317
322
|
try:
|
rbx/box/ui/css/app.tcss
CHANGED
@@ -7,18 +7,42 @@ Screen {
|
|
7
7
|
align: center middle;
|
8
8
|
}
|
9
9
|
|
10
|
+
ListView {
|
11
|
+
border: solid $accent 100%;
|
12
|
+
padding: 0 1;
|
13
|
+
background: transparent;
|
14
|
+
}
|
15
|
+
|
10
16
|
SelectionList {
|
11
17
|
border-title-color: $accent;
|
12
18
|
}
|
13
19
|
|
20
|
+
OptionList {
|
21
|
+
border: solid $accent;
|
22
|
+
}
|
23
|
+
|
14
24
|
Button {
|
15
25
|
width: 1fr;
|
16
26
|
background: $accent;
|
27
|
+
color: $secondary;
|
17
28
|
}
|
18
29
|
|
19
30
|
#report-grid {
|
20
31
|
layout: grid;
|
21
|
-
grid-size: 2
|
32
|
+
grid-size: 2;
|
33
|
+
}
|
34
|
+
|
35
|
+
RunScreen {
|
36
|
+
align: center middle;
|
37
|
+
#run-settings {
|
38
|
+
border: thick $background 40%;
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
LogDisplay {
|
43
|
+
width: 1fr;
|
44
|
+
padding: 0 1;
|
45
|
+
border: solid $accent;
|
22
46
|
}
|
23
47
|
|
24
48
|
DataTable {
|
@@ -27,7 +51,7 @@ DataTable {
|
|
27
51
|
|
28
52
|
DataTable > .datatable--cursor {
|
29
53
|
color: $text;
|
30
|
-
background: $accent
|
54
|
+
background-tint: $accent 10%;
|
31
55
|
}
|
32
56
|
|
33
57
|
DataTable:blur > .datatable--cursor {
|
@@ -35,14 +59,29 @@ DataTable:blur > .datatable--cursor {
|
|
35
59
|
background: transparent;
|
36
60
|
}
|
37
61
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
62
|
+
FileLog > Log {
|
63
|
+
background: transparent;
|
64
|
+
}
|
65
|
+
|
66
|
+
TestExplorerScreen {
|
67
|
+
#test-metadata {
|
68
|
+
min-height: 3;
|
69
|
+
height: auto;
|
70
|
+
}
|
71
|
+
#test-list-container {
|
72
|
+
min-width: 20;
|
73
|
+
max-width: 40;
|
74
|
+
height: 1fr;
|
42
75
|
}
|
43
|
-
|
76
|
+
#test-list {
|
44
77
|
width: 1fr;
|
78
|
+
}
|
79
|
+
FileLog {
|
80
|
+
border: solid $accent;
|
45
81
|
padding: 0 1;
|
82
|
+
}
|
83
|
+
RichLog {
|
46
84
|
border: solid $accent;
|
85
|
+
padding: 0 1;
|
47
86
|
}
|
48
|
-
}
|
87
|
+
}
|
rbx/box/ui/main.py
CHANGED
@@ -5,10 +5,14 @@ from textual.containers import Center
|
|
5
5
|
from textual.screen import Screen
|
6
6
|
from textual.widgets import Footer, Header, OptionList
|
7
7
|
|
8
|
-
from rbx.box.ui.
|
8
|
+
from rbx.box.ui.screens.build import BuildScreen
|
9
|
+
from rbx.box.ui.screens.run import RunScreen
|
10
|
+
from rbx.box.ui.screens.test_explorer import TestExplorerScreen
|
9
11
|
|
10
12
|
SCREEN_OPTIONS = [
|
11
13
|
('Run solutions against define testsets.', RunScreen),
|
14
|
+
('Build tests.', BuildScreen),
|
15
|
+
('Explore tests.', TestExplorerScreen),
|
12
16
|
]
|
13
17
|
|
14
18
|
|
File without changes
|