rbx.cp 0.5.53__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 +24 -4
- 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/grading/processing_context.py +43 -4
- rbx/grading/steps.py +58 -14
- 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.53.dist-info → rbx_cp-0.5.55.dist-info}/METADATA +8 -2
- {rbx_cp-0.5.53.dist-info → rbx_cp-0.5.55.dist-info}/RECORD +36 -28
- {rbx_cp-0.5.53.dist-info → rbx_cp-0.5.55.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.53.dist-info → rbx_cp-0.5.55.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.53.dist-info → rbx_cp-0.5.55.dist-info}/entry_points.txt +0 -0
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
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from textual.app import ComposeResult
|
5
|
+
from textual.screen import Screen
|
6
|
+
from textual.widgets import Footer, Header
|
7
|
+
|
8
|
+
from rbx.box.ui.captured_log import LogDisplay
|
9
|
+
|
10
|
+
|
11
|
+
class CommandScreen(Screen):
|
12
|
+
BINDINGS = [('q', 'app.pop_screen', 'Back')]
|
13
|
+
|
14
|
+
def __init__(self, command: List[str]):
|
15
|
+
super().__init__()
|
16
|
+
self.command = command
|
17
|
+
|
18
|
+
def compose(self) -> ComposeResult:
|
19
|
+
yield Header()
|
20
|
+
yield Footer()
|
21
|
+
yield LogDisplay()
|
22
|
+
|
23
|
+
async def _run_command(self):
|
24
|
+
exitcode = await self.query_one(LogDisplay).capture(self.command)
|
25
|
+
if exitcode != 0:
|
26
|
+
self.query_one(LogDisplay).border_subtitle = f'Exit code: {exitcode}'
|
27
|
+
return
|
28
|
+
|
29
|
+
self.query_one(LogDisplay).border_subtitle = 'Finished'
|
30
|
+
|
31
|
+
async def on_mount(self):
|
32
|
+
self.query_one(LogDisplay).border_title = 'Command output'
|
33
|
+
|
34
|
+
# Fire and forget.
|
35
|
+
asyncio.create_task(self._run_command())
|
@@ -18,9 +18,10 @@ from rbx.box.solutions import (
|
|
18
18
|
SolutionReportSkeleton,
|
19
19
|
get_evals_formatted_time,
|
20
20
|
get_testcase_markup_verdict,
|
21
|
-
run_solutions,
|
22
21
|
)
|
23
22
|
from rbx.box.ui.captured_log import LogDisplay, LogDisplayState
|
23
|
+
from rbx.box.ui.screens.command import CommandScreen
|
24
|
+
from rbx.grading.steps import Evaluation
|
24
25
|
|
25
26
|
|
26
27
|
def _build_solution_selection_label(sol: Solution) -> Text:
|
@@ -36,7 +37,7 @@ def _build_solution_selection_label(sol: Solution) -> Text:
|
|
36
37
|
class SolutionReportScreen(Screen):
|
37
38
|
skeleton: SolutionReportSkeleton
|
38
39
|
|
39
|
-
BINDINGS = [('q', 'app.pop_screen', '
|
40
|
+
BINDINGS = [('q', 'app.pop_screen', 'Back')]
|
40
41
|
|
41
42
|
def __init__(
|
42
43
|
self,
|
@@ -86,7 +87,7 @@ class SolutionReportScreen(Screen):
|
|
86
87
|
return i
|
87
88
|
raise
|
88
89
|
|
89
|
-
async def process(self, item: EvaluationItem):
|
90
|
+
async def process(self, item: EvaluationItem, eval: Evaluation):
|
90
91
|
pkg = package.find_problem_package_or_die()
|
91
92
|
sol_idx_in_skeleton = self._find_solution_index_in_skeleton(
|
92
93
|
pkg.solutions[item.solution_index]
|
@@ -101,17 +102,19 @@ class SolutionReportScreen(Screen):
|
|
101
102
|
|
102
103
|
table.update_cell_at(
|
103
104
|
Coordinate(row=row_idx, column=2),
|
104
|
-
get_testcase_markup_verdict(
|
105
|
+
get_testcase_markup_verdict(eval),
|
105
106
|
update_width=True,
|
106
107
|
)
|
107
108
|
table.update_cell_at(
|
108
109
|
Coordinate(row=row_idx, column=3),
|
109
|
-
get_evals_formatted_time([
|
110
|
+
get_evals_formatted_time([eval]),
|
110
111
|
update_width=True,
|
111
112
|
)
|
112
113
|
|
113
114
|
|
114
115
|
class RunScreen(Screen):
|
116
|
+
BINDINGS = [('q', 'app.pop_screen', 'Back')]
|
117
|
+
|
115
118
|
def compose(self) -> ComposeResult:
|
116
119
|
yield Header()
|
117
120
|
yield Footer()
|
@@ -141,7 +144,6 @@ class RunScreen(Screen):
|
|
141
144
|
id='run-config',
|
142
145
|
)
|
143
146
|
yield Button('Run')
|
144
|
-
yield LogDisplay()
|
145
147
|
|
146
148
|
def on_mount(self):
|
147
149
|
sols = self.query_one('#run-sols', SelectionList)
|
@@ -160,7 +162,6 @@ class RunScreen(Screen):
|
|
160
162
|
async def on_button_pressed(self, _: Button.Pressed):
|
161
163
|
await self.action_run()
|
162
164
|
|
163
|
-
@textual.work(thread=True)
|
164
165
|
async def _run_solutions(self, tracked_solutions: Set[str], check: bool):
|
165
166
|
main_solution = package.get_main_solution()
|
166
167
|
if check and main_solution is None:
|
@@ -169,36 +170,7 @@ class RunScreen(Screen):
|
|
169
170
|
)
|
170
171
|
check = False
|
171
172
|
|
172
|
-
|
173
|
-
return await self.query_one(LogDisplay).capture(['rbx', 'build'])
|
174
|
-
|
175
|
-
exitcode = self.app.call_from_thread(build)
|
176
|
-
|
177
|
-
if exitcode != 0:
|
178
|
-
textual.log(f'early quit: {exitcode}')
|
179
|
-
return
|
180
|
-
|
181
|
-
textual.log('build finished ok, running solutions')
|
182
|
-
|
183
|
-
res = run_solutions(tracked_solutions=tracked_solutions, check=check)
|
184
|
-
|
185
|
-
async def mount_report_widget() -> SolutionReportScreen:
|
186
|
-
# log_display_state = self.query_one(LogDisplay).export()
|
187
|
-
log_display_state = None
|
188
|
-
await self.app.push_screen(
|
189
|
-
screen := SolutionReportScreen(
|
190
|
-
res.skeleton, log_display_state=log_display_state
|
191
|
-
)
|
192
|
-
)
|
193
|
-
return screen
|
194
|
-
|
195
|
-
new_screen = await mount_report_widget()
|
196
|
-
|
197
|
-
async def process_item(item: EvaluationItem):
|
198
|
-
await new_screen.process(item)
|
199
|
-
|
200
|
-
for item in res.items:
|
201
|
-
self.app.call_from_thread(process_item, item)
|
173
|
+
self.app.switch_screen(CommandScreen(['rbx', 'run']))
|
202
174
|
|
203
175
|
async def action_run(self):
|
204
176
|
sols = self.query_one('#run-sols', SelectionList)
|
@@ -207,4 +179,4 @@ class RunScreen(Screen):
|
|
207
179
|
tracked_solutions = set(str(sol) for sol in sols.selected)
|
208
180
|
check = 'check' in config.selected
|
209
181
|
|
210
|
-
self._run_solutions(tracked_solutions, check)
|
182
|
+
await self._run_solutions(tracked_solutions, check)
|
@@ -0,0 +1,100 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
from textual.app import ComposeResult
|
4
|
+
from textual.containers import Horizontal, Vertical
|
5
|
+
from textual.screen import Screen
|
6
|
+
from textual.widgets import Footer, Header, Label, ListItem, ListView, RichLog
|
7
|
+
|
8
|
+
from rbx.box.testcase_extractors import (
|
9
|
+
GenerationTestcaseEntry,
|
10
|
+
extract_generation_testcases_from_groups,
|
11
|
+
)
|
12
|
+
from rbx.box.ui.widgets.file_log import FileLog
|
13
|
+
from rbx.box.ui.widgets.rich_log_box import RichLogBox
|
14
|
+
|
15
|
+
|
16
|
+
class TestExplorerScreen(Screen):
|
17
|
+
BINDINGS = [
|
18
|
+
('q', 'app.pop_screen', 'Quit'),
|
19
|
+
('m', 'toggle_metadata', 'Toggle metadata'),
|
20
|
+
]
|
21
|
+
|
22
|
+
def __init__(self):
|
23
|
+
super().__init__()
|
24
|
+
self._entries: List[GenerationTestcaseEntry] = []
|
25
|
+
|
26
|
+
def compose(self) -> ComposeResult:
|
27
|
+
yield Header()
|
28
|
+
yield Footer()
|
29
|
+
with Horizontal(id='test-explorer'):
|
30
|
+
with Vertical(id='test-list-container'):
|
31
|
+
yield ListView(id='test-list')
|
32
|
+
with Vertical(id='test-details'):
|
33
|
+
yield FileLog(id='test-input')
|
34
|
+
yield FileLog(id='test-output')
|
35
|
+
yield RichLogBox(id='test-metadata')
|
36
|
+
|
37
|
+
async def on_mount(self):
|
38
|
+
self.query_one('#test-list').border_title = 'Tests'
|
39
|
+
self.query_one('#test-input').border_title = 'Input'
|
40
|
+
self.query_one('#test-output').border_title = 'Output'
|
41
|
+
|
42
|
+
metadata = self.query_one('#test-metadata', RichLogBox)
|
43
|
+
metadata.display = False
|
44
|
+
metadata.border_title = 'Metadata'
|
45
|
+
metadata.wrap = True
|
46
|
+
metadata.markup = True
|
47
|
+
metadata.clear().write('No test selected')
|
48
|
+
await self._update_tests()
|
49
|
+
|
50
|
+
def action_toggle_metadata(self):
|
51
|
+
metadata = self.query_one('#test-metadata', RichLogBox)
|
52
|
+
metadata.display = not metadata.display
|
53
|
+
|
54
|
+
def _update_selected_test(self, index: Optional[int]):
|
55
|
+
input = self.query_one('#test-input', FileLog)
|
56
|
+
output = self.query_one('#test-output', FileLog)
|
57
|
+
metadata = self.query_one('#test-metadata', RichLog)
|
58
|
+
|
59
|
+
if index is None:
|
60
|
+
input.path = None
|
61
|
+
output.path = None
|
62
|
+
metadata.clear().write('No test selected')
|
63
|
+
return
|
64
|
+
entry = self._entries[index]
|
65
|
+
input.path = entry.metadata.copied_to.inputPath
|
66
|
+
output.path = entry.metadata.copied_to.outputPath
|
67
|
+
|
68
|
+
metadata.clear()
|
69
|
+
metadata.write(
|
70
|
+
f'[bold]{entry.group_entry.group}[/bold] / [bold]{entry.group_entry.index}[/bold]'
|
71
|
+
)
|
72
|
+
if entry.metadata.copied_from is not None:
|
73
|
+
metadata.write(
|
74
|
+
f'[bold]Copied from:[/bold] {entry.metadata.copied_from.inputPath}'
|
75
|
+
)
|
76
|
+
if entry.metadata.generator_call is not None:
|
77
|
+
metadata.write(f'[bold]Gen. call:[/bold] {entry.metadata.generator_call}')
|
78
|
+
if entry.metadata.generator_script is not None:
|
79
|
+
metadata.write(
|
80
|
+
f'[bold]Gen. script:[/bold] {entry.metadata.generator_script}'
|
81
|
+
)
|
82
|
+
|
83
|
+
async def _update_tests(self):
|
84
|
+
self.watch(
|
85
|
+
self.query_one('#test-list', ListView),
|
86
|
+
'index',
|
87
|
+
self._update_selected_test,
|
88
|
+
)
|
89
|
+
|
90
|
+
self._entries = await extract_generation_testcases_from_groups()
|
91
|
+
|
92
|
+
test_names = [
|
93
|
+
f'{entry.group_entry.group}/{entry.group_entry.index}'
|
94
|
+
for entry in self._entries
|
95
|
+
]
|
96
|
+
|
97
|
+
await self.query_one('#test-list', ListView).clear()
|
98
|
+
await self.query_one('#test-list', ListView).extend(
|
99
|
+
[ListItem(Label(name)) for name in test_names]
|
100
|
+
)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import pathlib
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import aiofiles
|
5
|
+
from textual import work
|
6
|
+
from textual.app import ComposeResult
|
7
|
+
from textual.reactive import reactive
|
8
|
+
from textual.widget import Widget
|
9
|
+
from textual.widgets import Log
|
10
|
+
|
11
|
+
BATCH_SIZE = 1024
|
12
|
+
|
13
|
+
|
14
|
+
class FileLog(Widget, can_focus=False):
|
15
|
+
DEFAULT_CSS = """
|
16
|
+
FileLog {
|
17
|
+
border: solid $accent;
|
18
|
+
height: 1fr;
|
19
|
+
width: 1fr;
|
20
|
+
}
|
21
|
+
"""
|
22
|
+
|
23
|
+
path: reactive[Optional[pathlib.Path]] = reactive(None)
|
24
|
+
|
25
|
+
def compose(self) -> ComposeResult:
|
26
|
+
yield Log()
|
27
|
+
|
28
|
+
def on_mount(self):
|
29
|
+
self.query_one(Log).auto_scroll = False
|
30
|
+
self.query_one(Log).can_focus = False
|
31
|
+
|
32
|
+
@work(exclusive=True)
|
33
|
+
async def _load_file(self, path: pathlib.Path):
|
34
|
+
log = self.query_one(Log)
|
35
|
+
log.clear()
|
36
|
+
path_str = str(path.relative_to(pathlib.Path.cwd()))
|
37
|
+
self.border_subtitle = f'{path_str} (loading...)'
|
38
|
+
|
39
|
+
async with aiofiles.open(path, 'r') as f:
|
40
|
+
batch = []
|
41
|
+
async for line in f:
|
42
|
+
batch.append(line)
|
43
|
+
if len(batch) >= BATCH_SIZE:
|
44
|
+
log.write(''.join(batch))
|
45
|
+
batch = []
|
46
|
+
|
47
|
+
if batch:
|
48
|
+
log.write(''.join(batch))
|
49
|
+
|
50
|
+
self.border_subtitle = path_str
|
51
|
+
|
52
|
+
async def watch_path(self, path: Optional[pathlib.Path]):
|
53
|
+
log = self.query_one(Log)
|
54
|
+
log.clear()
|
55
|
+
|
56
|
+
if path is None:
|
57
|
+
return
|
58
|
+
|
59
|
+
if not path.is_file():
|
60
|
+
self.query_one(Log).write(f'File {path} does not exist')
|
61
|
+
return
|
62
|
+
|
63
|
+
self._load_file(path)
|
@@ -322,7 +322,11 @@ class StupidSandbox(SandboxBase):
|
|
322
322
|
return self.translate_box_exitcode(self.returncode)
|
323
323
|
|
324
324
|
def translate_box_exitcode(self, exitcode: int) -> bool:
|
325
|
-
# SIGALRM can be safely ignored, just in case it leaks away.
|
325
|
+
# SIGALRM can be safely ignored, just in case it leaks away. SIGTERM also.
|
326
|
+
if self.log is None:
|
327
|
+
return False
|
328
|
+
if 'TE' in self.get_status_list():
|
329
|
+
return True
|
326
330
|
return super().translate_box_exitcode(exitcode) or -exitcode == signal.SIGALRM
|
327
331
|
|
328
332
|
def debug_message(self) -> Any:
|
@@ -109,7 +109,8 @@ def create_tee(files, mode, buffer_size=4096, prefix=''):
|
|
109
109
|
else:
|
110
110
|
# Parent -- Return a file object wrapper around the pipe to the
|
111
111
|
# child.
|
112
|
-
|
112
|
+
# Preserve line buffering (buffering=1).
|
113
|
+
return os.fdopen(pipe_write, 'w', buffering=1, closefd=False)
|
113
114
|
|
114
115
|
|
115
116
|
def parse_opts() -> Options:
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
import contextlib
|
2
3
|
import os
|
3
4
|
import signal
|
@@ -5,22 +6,26 @@ import threading
|
|
5
6
|
from typing import Optional, Set
|
6
7
|
|
7
8
|
_processing_context_pids: Optional[Set[int]] = None
|
9
|
+
_terminate_all_on_error = False
|
8
10
|
_lock = threading.Lock()
|
9
11
|
|
10
12
|
# Creating a processing context is not thread-safe, but adding to it is.
|
11
13
|
|
12
14
|
|
13
15
|
@contextlib.contextmanager
|
14
|
-
def new_processing_context():
|
15
|
-
global _processing_context_pids
|
16
|
+
def new_processing_context(terminate_all_on_error: bool = False):
|
17
|
+
global _processing_context_pids, _terminate_all_on_error
|
16
18
|
with _lock:
|
17
19
|
old_processing_context_pids = _processing_context_pids
|
20
|
+
_old_terminate_all_on_error = _terminate_all_on_error
|
18
21
|
_processing_context_pids = set()
|
22
|
+
_terminate_all_on_error = terminate_all_on_error
|
19
23
|
try:
|
20
24
|
yield
|
21
25
|
finally:
|
22
26
|
with _lock:
|
23
27
|
_processing_context_pids = old_processing_context_pids
|
28
|
+
_terminate_all_on_error = _old_terminate_all_on_error
|
24
29
|
|
25
30
|
|
26
31
|
def get_processing_context() -> Set[int]:
|
@@ -36,7 +41,8 @@ def add_to_processing_context(pid: int):
|
|
36
41
|
_processing_context_pids.add(pid)
|
37
42
|
|
38
43
|
|
39
|
-
def terminate_all_processes_in_context():
|
44
|
+
def terminate_all_processes_in_context(clear: bool = True):
|
45
|
+
global _processing_context_pids
|
40
46
|
with _lock:
|
41
47
|
if _processing_context_pids is None:
|
42
48
|
return
|
@@ -45,4 +51,37 @@ def terminate_all_processes_in_context():
|
|
45
51
|
os.kill(pid, signal.SIGTERM)
|
46
52
|
except OSError:
|
47
53
|
pass
|
48
|
-
|
54
|
+
if clear:
|
55
|
+
_processing_context_pids.clear()
|
56
|
+
|
57
|
+
|
58
|
+
async def wait_all_processes_in_context(wait_for: int):
|
59
|
+
global _processing_context_pids, _terminate_all_on_error
|
60
|
+
wait_pids = set()
|
61
|
+
while len(get_processing_context()) < wait_for:
|
62
|
+
await asyncio.sleep(0.01)
|
63
|
+
|
64
|
+
with _lock:
|
65
|
+
if _processing_context_pids is None:
|
66
|
+
return
|
67
|
+
wait_pids.update(_processing_context_pids)
|
68
|
+
|
69
|
+
wait_lock = threading.Lock()
|
70
|
+
finished_pids = []
|
71
|
+
|
72
|
+
def process(pid: int, returncode: int):
|
73
|
+
with wait_lock:
|
74
|
+
finished_pids.append(pid)
|
75
|
+
if returncode != 0 and _terminate_all_on_error:
|
76
|
+
terminate_all_processes_in_context()
|
77
|
+
|
78
|
+
def wait_all_processes():
|
79
|
+
while len(finished_pids) < len(wait_pids):
|
80
|
+
try:
|
81
|
+
pid, status = os.wait()
|
82
|
+
except ChildProcessError:
|
83
|
+
return
|
84
|
+
if pid in wait_pids:
|
85
|
+
process(pid, os.waitstatus_to_exitcode(status))
|
86
|
+
|
87
|
+
await asyncio.to_thread(wait_all_processes)
|
rbx/grading/steps.py
CHANGED
@@ -193,6 +193,11 @@ class RunLogMetadata(BaseModel):
|
|
193
193
|
retryIndex: Optional[int] = None
|
194
194
|
|
195
195
|
|
196
|
+
class ProcessingContextLog(BaseModel):
|
197
|
+
pid: int = -1
|
198
|
+
exitindex: int = -1
|
199
|
+
|
200
|
+
|
196
201
|
class RunLog(BaseModel):
|
197
202
|
exitcode: int = 0
|
198
203
|
exitstatus: str = SandboxBase.EXIT_SANDBOX_ERROR
|
@@ -330,14 +335,29 @@ def _expand_part(part: str, sandbox: SandboxBase) -> List[str]:
|
|
330
335
|
return [part]
|
331
336
|
|
332
337
|
|
338
|
+
def _get_java_memory_limits(sandbox: SandboxBase) -> Tuple[int, int]:
|
339
|
+
max_memory = sandbox.params.address_space
|
340
|
+
if max_memory is None:
|
341
|
+
max_memory = 2048
|
342
|
+
return max_memory, min(512, int(max_memory * 0.9))
|
343
|
+
|
344
|
+
|
333
345
|
def _split_and_expand(command: str, sandbox: SandboxBase) -> List[str]:
|
334
346
|
res = []
|
335
|
-
|
347
|
+
max_mem, init_mem = _get_java_memory_limits(sandbox)
|
348
|
+
parts = shlex.split(command.format(memory=max_mem, initialMemory=init_mem))
|
336
349
|
for part in parts:
|
337
350
|
res.extend(_expand_part(part, sandbox))
|
338
351
|
return res
|
339
352
|
|
340
353
|
|
354
|
+
def get_exe_from_command(command: str) -> str:
|
355
|
+
cmds = shlex.split(command)
|
356
|
+
if not cmds:
|
357
|
+
return command
|
358
|
+
return cmds[0]
|
359
|
+
|
360
|
+
|
341
361
|
def _is_c_command(exe_command: str) -> bool:
|
342
362
|
return 'gcc' in exe_command or 'clang' in exe_command
|
343
363
|
|
@@ -351,15 +371,26 @@ def is_cxx_command(exe_command: str) -> bool:
|
|
351
371
|
|
352
372
|
|
353
373
|
def is_cxx_sanitizer_command(command: str) -> bool:
|
354
|
-
|
355
|
-
if not
|
374
|
+
exe = get_exe_from_command(command)
|
375
|
+
if not exe:
|
356
376
|
return False
|
357
|
-
exe = cmds[0]
|
358
377
|
if not is_cxx_command(exe):
|
359
378
|
return False
|
360
379
|
return 'fsanitize' in command
|
361
380
|
|
362
381
|
|
382
|
+
def is_java_command(exe_command: str) -> bool:
|
383
|
+
return 'javac' in exe_command or 'java' in exe_command
|
384
|
+
|
385
|
+
|
386
|
+
def is_kotlin_command(exe_command: str) -> bool:
|
387
|
+
return 'kotlinc' in exe_command or 'kotlin' in exe_command
|
388
|
+
|
389
|
+
|
390
|
+
def is_java_like_command(exe_command: str) -> bool:
|
391
|
+
return is_java_command(exe_command) or is_kotlin_command(exe_command)
|
392
|
+
|
393
|
+
|
363
394
|
@functools.cache
|
364
395
|
def _complain_about_clang() -> None:
|
365
396
|
console.print(
|
@@ -539,6 +570,10 @@ def compile(
|
|
539
570
|
stderr_file = pathlib.PosixPath(f'compile-{i}.stderr')
|
540
571
|
sandbox.params.set_stdall(stdout=stdout_file, stderr=stderr_file)
|
541
572
|
|
573
|
+
# Remove memory constraints for Java.
|
574
|
+
if is_java_like_command(get_exe_from_command(command)):
|
575
|
+
sandbox.params.address_space = None
|
576
|
+
|
542
577
|
if bits_artifact is not None and _is_cpp_command(cmd[0]):
|
543
578
|
# Include from sandbox directory to import bits/stdc++.h.
|
544
579
|
cmd.append('-I.')
|
@@ -604,6 +639,10 @@ async def run(
|
|
604
639
|
cmd = _split_and_expand(command, sandbox)
|
605
640
|
sandbox.set_params(params)
|
606
641
|
|
642
|
+
# Remove memory constraints for Java.
|
643
|
+
if is_java_like_command(get_exe_from_command(command)):
|
644
|
+
sandbox.params.address_space = None
|
645
|
+
|
607
646
|
if not await asyncio.to_thread(sandbox.execute_without_std, cmd):
|
608
647
|
console.print(
|
609
648
|
'[error]Sandbox crashed while processing command:[/error]',
|
@@ -614,7 +653,7 @@ async def run(
|
|
614
653
|
return None
|
615
654
|
|
616
655
|
if sandbox.get_exit_code() != 0 and kill_on_processing_context_exit:
|
617
|
-
processing_context.terminate_all_processes_in_context()
|
656
|
+
processing_context.terminate_all_processes_in_context(clear=False)
|
618
657
|
|
619
658
|
if not _process_output_artifacts(artifacts, sandbox):
|
620
659
|
return None
|
@@ -657,22 +696,27 @@ async def run_coordinated(
|
|
657
696
|
interactor: CoordinatedRunParams,
|
658
697
|
solution: CoordinatedRunParams,
|
659
698
|
) -> Tuple[Optional[RunLog], Optional[RunLog]]:
|
660
|
-
with processing_context.new_processing_context():
|
699
|
+
with processing_context.new_processing_context(terminate_all_on_error=True):
|
700
|
+
# Schedule both runs to execute immediately.
|
661
701
|
runs = tuple(
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
702
|
+
asyncio.create_task(
|
703
|
+
run(
|
704
|
+
params.command,
|
705
|
+
params.params,
|
706
|
+
params.sandbox,
|
707
|
+
params.artifacts,
|
708
|
+
params.metadata,
|
709
|
+
kill_on_processing_context_exit=True,
|
710
|
+
)
|
669
711
|
)
|
670
712
|
for params in [interactor, solution]
|
671
713
|
)
|
672
|
-
|
714
|
+
await processing_context.wait_all_processes_in_context(wait_for=2)
|
715
|
+
logs = typing.cast(
|
673
716
|
Tuple[Optional[RunLog], Optional[RunLog]],
|
674
717
|
tuple(await asyncio.gather(*runs)),
|
675
718
|
)
|
719
|
+
return logs
|
676
720
|
|
677
721
|
|
678
722
|
def _normalize_checked_words(s: str) -> Tuple[str, ...]:
|
@@ -146,8 +146,15 @@ echo "solution \$SFPID -> \$ECSF" >&2
|
|
146
146
|
echo "interactor exitcode \$ECINT" >&2
|
147
147
|
echo "solution exitcode \$ECSF" >&2
|
148
148
|
|
149
|
+
RTE=0
|
150
|
+
if [[ \$ECSF -eq -13 ]]; then
|
151
|
+
RTE=0
|
152
|
+
elif [[ \$ECSF -ne 0 ]] && cat stderr0 | grep -q "wrong output format Unexpected end of file"; then
|
153
|
+
RTE=1
|
154
|
+
fi
|
155
|
+
|
149
156
|
ret=0
|
150
|
-
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]]; then
|
157
|
+
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]] && [[ \$RTE -eq 0 ]]; then
|
151
158
|
echo "testlib exitcode \$ECINT" >stdout0
|
152
159
|
ret=0
|
153
160
|
elif [[ \$ECSF -ne 0 ]]; then
|
@@ -146,8 +146,15 @@ echo "solution \$SFPID -> \$ECSF" >&2
|
|
146
146
|
echo "interactor exitcode \$ECINT" >&2
|
147
147
|
echo "solution exitcode \$ECSF" >&2
|
148
148
|
|
149
|
+
RTE=0
|
150
|
+
if [[ \$ECSF -eq -13 ]]; then
|
151
|
+
RTE=0
|
152
|
+
elif [[ \$ECSF -ne 0 ]] && cat stderr0 | grep -q "wrong output format Unexpected end of file"; then
|
153
|
+
RTE=1
|
154
|
+
fi
|
155
|
+
|
149
156
|
ret=0
|
150
|
-
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]]; then
|
157
|
+
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]] && [[ \$RTE -eq 0 ]]; then
|
151
158
|
echo "testlib exitcode \$ECINT" >stdout0
|
152
159
|
ret=0
|
153
160
|
elif [[ \$ECSF -ne 0 ]]; then
|
@@ -146,8 +146,15 @@ echo "solution \$SFPID -> \$ECSF" >&2
|
|
146
146
|
echo "interactor exitcode \$ECINT" >&2
|
147
147
|
echo "solution exitcode \$ECSF" >&2
|
148
148
|
|
149
|
+
RTE=0
|
150
|
+
if [[ \$ECSF -eq -13 ]]; then
|
151
|
+
RTE=0
|
152
|
+
elif [[ \$ECSF -ne 0 ]] && cat stderr0 | grep -q "wrong output format Unexpected end of file"; then
|
153
|
+
RTE=1
|
154
|
+
fi
|
155
|
+
|
149
156
|
ret=0
|
150
|
-
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]]; then
|
157
|
+
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]] && [[ \$RTE -eq 0 ]]; then
|
151
158
|
echo "testlib exitcode \$ECINT" >stdout0
|
152
159
|
ret=0
|
153
160
|
elif [[ \$ECSF -ne 0 ]]; then
|
@@ -159,8 +159,15 @@ echo "solution \$SFPID -> \$ECSF" >&2
|
|
159
159
|
echo "interactor exitcode \$ECINT" >&2
|
160
160
|
echo "solution exitcode \$ECSF" >&2
|
161
161
|
|
162
|
+
RTE=0
|
163
|
+
if [[ \$ECSF -eq -13 ]]; then
|
164
|
+
RTE=0
|
165
|
+
elif [[ \$ECSF -ne 0 ]] && cat stderr0 | grep -q "wrong output format Unexpected end of file"; then
|
166
|
+
RTE=1
|
167
|
+
fi
|
168
|
+
|
162
169
|
ret=0
|
163
|
-
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]]; then
|
170
|
+
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]] && [[ \$RTE -eq 0 ]]; then
|
164
171
|
echo "testlib exitcode \$ECINT" >stdout0
|
165
172
|
ret=0
|
166
173
|
elif [[ \$ECSF -ne 0 ]]; then
|
@@ -150,8 +150,15 @@ echo "solution \$SFPID -> \$ECSF" >&2
|
|
150
150
|
echo "interactor exitcode \$ECINT" >&2
|
151
151
|
echo "solution exitcode \$ECSF" >&2
|
152
152
|
|
153
|
+
RTE=0
|
154
|
+
if [[ \$ECSF -eq -13 ]]; then
|
155
|
+
RTE=0
|
156
|
+
elif [[ \$ECSF -ne 0 ]] && cat stderr0 | grep -q "wrong output format Unexpected end of file"; then
|
157
|
+
RTE=1
|
158
|
+
fi
|
159
|
+
|
153
160
|
ret=0
|
154
|
-
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]]; then
|
161
|
+
if [[ \$ECINT -ge 1 ]] && [[ \$ECINT -le 4 ]] && [[ \$RTE -eq 0 ]]; then
|
155
162
|
echo "testlib exitcode \$ECINT" >stdout0
|
156
163
|
ret=0
|
157
164
|
elif [[ \$ECSF -ne 0 ]]; then
|