rbx.cp 0.5.61__py3-none-any.whl → 0.5.63__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/cd.py +14 -0
- rbx/box/cli.py +6 -0
- rbx/box/code.py +34 -5
- rbx/box/contest/main.py +6 -2
- rbx/box/git_utils.py +28 -0
- rbx/box/package.py +23 -0
- rbx/box/packaging/boca/packager.py +3 -18
- rbx/box/packaging/moj/packager.py +1 -1
- rbx/box/packaging/polygon/upload.py +7 -5
- rbx/box/presets/__init__.py +80 -6
- rbx/box/presets/fetch.py +18 -1
- rbx/box/retries.py +2 -0
- rbx/box/solutions.py +242 -114
- rbx/box/solutions_test.py +3 -1
- rbx/box/tasks.py +6 -1
- rbx/box/testcase_utils.py +3 -0
- rbx/box/ui/css/app.tcss +14 -2
- rbx/box/ui/main.py +3 -5
- rbx/box/ui/screens/error.py +19 -0
- rbx/box/ui/screens/run.py +4 -12
- rbx/box/ui/screens/run_explorer.py +77 -1
- rbx/box/ui/screens/run_test_explorer.py +166 -0
- rbx/box/ui/screens/selector.py +26 -0
- rbx/box/ui/screens/test_explorer.py +33 -5
- rbx/box/ui/utils/__init__.py +0 -0
- rbx/box/ui/utils/run_ui.py +95 -0
- rbx/box/ui/widgets/__init__.py +0 -0
- rbx/box/ui/widgets/file_log.py +3 -1
- rbx/box/ui/widgets/interaction_box.py +59 -0
- rbx/box/ui/widgets/test_output_box.py +113 -0
- rbx/box/ui/widgets/two_sided_test_output_box.py +60 -0
- rbx/grading/steps.py +1 -0
- rbx/resources/packagers/boca/compile/java +55 -59
- rbx/resources/packagers/boca/interactive/java +2 -2
- rbx/resources/packagers/boca/run/java +2 -2
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/METADATA +1 -1
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/RECORD +40 -30
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/entry_points.txt +0 -0
rbx/box/ui/css/app.tcss
CHANGED
@@ -63,8 +63,11 @@ FileLog > Log {
|
|
63
63
|
background: transparent;
|
64
64
|
}
|
65
65
|
|
66
|
-
TestExplorerScreen {
|
67
|
-
#test-
|
66
|
+
TestExplorerScreen, RunTestExplorerScreen {
|
67
|
+
#test-box-switcher {
|
68
|
+
height: 1fr;
|
69
|
+
}
|
70
|
+
#test-metadata, #test-box-metadata {
|
68
71
|
min-height: 3;
|
69
72
|
height: auto;
|
70
73
|
}
|
@@ -76,6 +79,15 @@ TestExplorerScreen {
|
|
76
79
|
#test-list {
|
77
80
|
width: 1fr;
|
78
81
|
}
|
82
|
+
TestBoxWidget, TwoSidedTestBoxWidget {
|
83
|
+
height: 1fr;
|
84
|
+
#test-box-1 {
|
85
|
+
width: 1fr;
|
86
|
+
}
|
87
|
+
#test-box-2 {
|
88
|
+
width: 1fr;
|
89
|
+
}
|
90
|
+
}
|
79
91
|
FileLog {
|
80
92
|
border: solid $accent;
|
81
93
|
padding: 0 1;
|
rbx/box/ui/main.py
CHANGED
@@ -5,14 +5,12 @@ 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.screens.
|
9
|
-
from rbx.box.ui.screens.run import RunScreen
|
8
|
+
from rbx.box.ui.screens.run_explorer import RunExplorerScreen
|
10
9
|
from rbx.box.ui.screens.test_explorer import TestExplorerScreen
|
11
10
|
|
12
11
|
SCREEN_OPTIONS = [
|
13
|
-
('
|
14
|
-
('
|
15
|
-
('Explore tests.', TestExplorerScreen),
|
12
|
+
('Explore tests built by `rbx build`.', TestExplorerScreen),
|
13
|
+
('Explore results of a past `rbx run`.', RunExplorerScreen),
|
16
14
|
]
|
17
15
|
|
18
16
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from textual.app import ComposeResult
|
2
|
+
from textual.screen import Screen
|
3
|
+
from textual.widgets import Footer, Header, Label
|
4
|
+
|
5
|
+
|
6
|
+
class ErrorScreen(Screen):
|
7
|
+
BINDINGS = [('q', 'app.pop_screen', 'Quit')]
|
8
|
+
|
9
|
+
def __init__(self, message: str):
|
10
|
+
super().__init__()
|
11
|
+
self.message = message
|
12
|
+
|
13
|
+
def compose(self) -> ComposeResult:
|
14
|
+
yield Header()
|
15
|
+
yield Footer()
|
16
|
+
yield Label(self.message)
|
17
|
+
|
18
|
+
def on_mount(self):
|
19
|
+
self.query_one(Label).border_title = 'Error'
|
rbx/box/ui/screens/run.py
CHANGED
@@ -81,20 +81,12 @@ class SolutionReportScreen(Screen):
|
|
81
81
|
if self.log_display_state is not None:
|
82
82
|
self.query_one(LogDisplay).load(self.log_display_state)
|
83
83
|
|
84
|
-
def _find_solution_index_in_skeleton(self, sol: Solution) -> int:
|
85
|
-
for i, solution in enumerate(self.skeleton.solutions):
|
86
|
-
if solution.path == sol.path:
|
87
|
-
return i
|
88
|
-
raise
|
89
|
-
|
90
84
|
async def process(self, item: EvaluationItem, eval: Evaluation):
|
91
|
-
|
92
|
-
sol_idx_in_skeleton
|
93
|
-
|
94
|
-
)
|
95
|
-
group = self.skeleton.find_group_skeleton(item.group_name)
|
85
|
+
sol_idx_in_skeleton = self.skeleton.find_solution_skeleton_index(item.solution)
|
86
|
+
assert sol_idx_in_skeleton is not None
|
87
|
+
group = self.skeleton.find_group_skeleton(item.testcase_entry.group)
|
96
88
|
assert group is not None
|
97
|
-
tc = group.testcases[item.
|
89
|
+
tc = group.testcases[item.testcase_entry.index]
|
98
90
|
|
99
91
|
textual.log(len(list(self.query(DataTable))), sol_idx_in_skeleton)
|
100
92
|
table = self.query(DataTable)[sol_idx_in_skeleton]
|
@@ -1,5 +1,81 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from textual.app import ComposeResult
|
4
|
+
from textual.reactive import reactive
|
1
5
|
from textual.screen import Screen
|
6
|
+
from textual.widgets import Footer, Header, Label, ListItem, ListView
|
7
|
+
|
8
|
+
from rbx.box.solutions import SolutionReportSkeleton
|
9
|
+
from rbx.box.ui.screens.error import ErrorScreen
|
10
|
+
from rbx.box.ui.screens.run_test_explorer import RunTestExplorerScreen
|
11
|
+
from rbx.box.ui.screens.selector import SelectorScreen
|
12
|
+
from rbx.box.ui.utils.run_ui import get_skeleton, get_solution_markup, has_run
|
2
13
|
|
3
14
|
|
4
15
|
class RunExplorerScreen(Screen):
|
5
|
-
|
16
|
+
BINDINGS = [('s', 'compare_with', 'Compare with')]
|
17
|
+
|
18
|
+
skeleton: reactive[Optional[SolutionReportSkeleton]] = reactive(
|
19
|
+
None, recompose=True
|
20
|
+
)
|
21
|
+
|
22
|
+
def compose(self) -> ComposeResult:
|
23
|
+
yield Header()
|
24
|
+
yield Footer()
|
25
|
+
|
26
|
+
items = []
|
27
|
+
if self.skeleton:
|
28
|
+
items = [
|
29
|
+
Label(get_solution_markup(self.skeleton, sol), markup=True)
|
30
|
+
for i, sol in enumerate(self.skeleton.solutions)
|
31
|
+
]
|
32
|
+
run_list = ListView(*[ListItem(item) for item in items], id='run-list')
|
33
|
+
run_list.border_title = 'Runs'
|
34
|
+
yield run_list
|
35
|
+
|
36
|
+
def on_mount(self):
|
37
|
+
if not has_run():
|
38
|
+
self.app.switch_screen(ErrorScreen('No runs found. Run `rbx run` first.'))
|
39
|
+
return
|
40
|
+
|
41
|
+
self.skeleton = get_skeleton()
|
42
|
+
|
43
|
+
def on_list_view_selected(self, event: ListView.Selected):
|
44
|
+
selected_index = event.list_view.index
|
45
|
+
if selected_index is None:
|
46
|
+
return
|
47
|
+
if self.skeleton is None:
|
48
|
+
return
|
49
|
+
self.app.push_screen(
|
50
|
+
RunTestExplorerScreen(
|
51
|
+
self.skeleton, self.skeleton.solutions[selected_index]
|
52
|
+
)
|
53
|
+
)
|
54
|
+
|
55
|
+
def action_compare_with(self):
|
56
|
+
if self.skeleton is None:
|
57
|
+
return
|
58
|
+
list_view = self.query_one('#run-list', ListView)
|
59
|
+
if list_view.index is None:
|
60
|
+
return
|
61
|
+
test_solution = self.skeleton.solutions[list_view.index]
|
62
|
+
|
63
|
+
options = [
|
64
|
+
ListItem(Label(f'{sol.path}', markup=False))
|
65
|
+
for sol in self.skeleton.solutions
|
66
|
+
]
|
67
|
+
|
68
|
+
def on_selected(index: Optional[int]):
|
69
|
+
if index is None:
|
70
|
+
return
|
71
|
+
if self.skeleton is None:
|
72
|
+
return
|
73
|
+
base_solution = self.skeleton.solutions[index]
|
74
|
+
self.app.push_screen(
|
75
|
+
RunTestExplorerScreen(self.skeleton, test_solution, base_solution)
|
76
|
+
)
|
77
|
+
|
78
|
+
self.app.push_screen(
|
79
|
+
SelectorScreen(options, title='Select a solution to compare against'),
|
80
|
+
callback=on_selected,
|
81
|
+
)
|
@@ -0,0 +1,166 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
from textual.app import ComposeResult
|
4
|
+
from textual.containers import Horizontal, Vertical
|
5
|
+
from textual.reactive import reactive
|
6
|
+
from textual.screen import Screen
|
7
|
+
from textual.widgets import Footer, Header, Label, ListItem, ListView
|
8
|
+
|
9
|
+
from rbx.box import package
|
10
|
+
from rbx.box.schema import TaskType
|
11
|
+
from rbx.box.solutions import SolutionReportSkeleton, SolutionSkeleton
|
12
|
+
from rbx.box.testcase_extractors import (
|
13
|
+
GenerationTestcaseEntry,
|
14
|
+
extract_generation_testcases,
|
15
|
+
)
|
16
|
+
from rbx.box.ui.utils.run_ui import (
|
17
|
+
get_run_testcase_markup,
|
18
|
+
get_run_testcase_metadata_markup,
|
19
|
+
)
|
20
|
+
from rbx.box.ui.widgets.file_log import FileLog
|
21
|
+
from rbx.box.ui.widgets.test_output_box import TestcaseRenderingData
|
22
|
+
from rbx.box.ui.widgets.two_sided_test_output_box import TwoSidedTestBoxWidget
|
23
|
+
|
24
|
+
|
25
|
+
class RunTestExplorerScreen(Screen):
|
26
|
+
BINDINGS = [
|
27
|
+
('q', 'app.pop_screen', 'Quit'),
|
28
|
+
('1', 'show_output', 'Show output'),
|
29
|
+
('2', 'show_stderr', 'Show stderr'),
|
30
|
+
('3', 'show_log', 'Show log'),
|
31
|
+
('m', 'toggle_metadata', 'Toggle metadata'),
|
32
|
+
('s', 'toggle_side_by_side', 'Toggle sxs'),
|
33
|
+
]
|
34
|
+
|
35
|
+
side_by_side: reactive[bool] = reactive(False)
|
36
|
+
diff_with_data: reactive[Optional[TestcaseRenderingData]] = reactive(
|
37
|
+
default=None,
|
38
|
+
)
|
39
|
+
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
skeleton: SolutionReportSkeleton,
|
43
|
+
solution: SolutionSkeleton,
|
44
|
+
diff_solution: Optional[SolutionSkeleton] = None,
|
45
|
+
):
|
46
|
+
super().__init__()
|
47
|
+
self.skeleton = skeleton
|
48
|
+
self.solution = solution
|
49
|
+
self.diff_solution = diff_solution
|
50
|
+
self.set_reactive(RunTestExplorerScreen.side_by_side, diff_solution is not None)
|
51
|
+
|
52
|
+
self._entries: List[GenerationTestcaseEntry] = []
|
53
|
+
|
54
|
+
def compose(self) -> ComposeResult:
|
55
|
+
yield Header()
|
56
|
+
yield Footer()
|
57
|
+
with Horizontal(id='test-explorer'):
|
58
|
+
with Vertical(id='test-list-container'):
|
59
|
+
yield ListView(id='test-list')
|
60
|
+
with Vertical(id='test-details'):
|
61
|
+
yield FileLog(id='test-input')
|
62
|
+
yield TwoSidedTestBoxWidget(id='test-output')
|
63
|
+
|
64
|
+
async def on_mount(self):
|
65
|
+
self.query_one('#test-list').border_title = 'Tests'
|
66
|
+
self.query_one('#test-input').border_title = 'Input'
|
67
|
+
|
68
|
+
# Ensure the output is show, even for interactive tests
|
69
|
+
self.action_show_output()
|
70
|
+
|
71
|
+
await self._update_tests()
|
72
|
+
|
73
|
+
def _get_rendering_data(
|
74
|
+
self, solution: SolutionSkeleton, entry: GenerationTestcaseEntry
|
75
|
+
) -> TestcaseRenderingData:
|
76
|
+
rendering_data = TestcaseRenderingData.from_one_path(
|
77
|
+
solution.get_entry_prefix(entry.group_entry)
|
78
|
+
)
|
79
|
+
rendering_data.rich_content = get_run_testcase_metadata_markup(
|
80
|
+
self.skeleton, solution, entry.group_entry
|
81
|
+
)
|
82
|
+
return rendering_data
|
83
|
+
|
84
|
+
def _update_selected_test(self, index: Optional[int]):
|
85
|
+
input = self.query_one('#test-input', FileLog)
|
86
|
+
output = self.query_one('#test-output', TwoSidedTestBoxWidget)
|
87
|
+
|
88
|
+
if index is None:
|
89
|
+
input.path = None
|
90
|
+
output.reset()
|
91
|
+
return
|
92
|
+
entry = self._entries[index]
|
93
|
+
input.path = entry.metadata.copied_to.inputPath
|
94
|
+
output.data = self._get_rendering_data(self.solution, entry)
|
95
|
+
|
96
|
+
if self.diff_solution is not None:
|
97
|
+
self.diff_with_data = self._get_rendering_data(self.diff_solution, entry)
|
98
|
+
else:
|
99
|
+
self.diff_with_data = TestcaseRenderingData.from_one_path(
|
100
|
+
entry.group_entry.get_prefix_path()
|
101
|
+
)
|
102
|
+
|
103
|
+
async def _update_tests(self):
|
104
|
+
self.watch(
|
105
|
+
self.query_one('#test-list', ListView),
|
106
|
+
'index',
|
107
|
+
self._update_selected_test,
|
108
|
+
)
|
109
|
+
|
110
|
+
self._entries = await extract_generation_testcases(self.skeleton.entries)
|
111
|
+
|
112
|
+
test_markups = [
|
113
|
+
get_run_testcase_markup(self.solution, entry.group_entry)
|
114
|
+
for entry in self._entries
|
115
|
+
]
|
116
|
+
|
117
|
+
await self.query_one('#test-list', ListView).clear()
|
118
|
+
await self.query_one('#test-list', ListView).extend(
|
119
|
+
[ListItem(Label(name, markup=True)) for name in test_markups]
|
120
|
+
)
|
121
|
+
|
122
|
+
def has_diffable_solution(self) -> bool:
|
123
|
+
return self.diff_solution is not None or package.get_main_solution() is not None
|
124
|
+
|
125
|
+
def should_show_interaction(self) -> bool:
|
126
|
+
pkg = package.find_problem_package_or_die()
|
127
|
+
return pkg.type == TaskType.COMMUNICATION and self.skeleton.capture_pipes
|
128
|
+
|
129
|
+
def action_show_output(self):
|
130
|
+
if self.should_show_interaction():
|
131
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).show_interaction()
|
132
|
+
else:
|
133
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).show_output()
|
134
|
+
|
135
|
+
def action_show_stderr(self):
|
136
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).show_stderr()
|
137
|
+
|
138
|
+
def action_show_log(self):
|
139
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).show_log()
|
140
|
+
|
141
|
+
def action_toggle_metadata(self):
|
142
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).toggle_metadata()
|
143
|
+
|
144
|
+
def action_toggle_side_by_side(self):
|
145
|
+
self.side_by_side = not self.side_by_side
|
146
|
+
|
147
|
+
def watch_side_by_side(self, side_by_side: bool):
|
148
|
+
widget = self.query_one('#test-output', TwoSidedTestBoxWidget)
|
149
|
+
|
150
|
+
if side_by_side:
|
151
|
+
if not self.has_diffable_solution():
|
152
|
+
self.app.notify(
|
153
|
+
'Found no solution to compare against', severity='error'
|
154
|
+
)
|
155
|
+
return
|
156
|
+
widget.diff_with_data = self.diff_with_data
|
157
|
+
else:
|
158
|
+
widget.diff_with_data = None
|
159
|
+
|
160
|
+
def watch_diff_with_data(self, diff_with_data: Optional[TestcaseRenderingData]):
|
161
|
+
if not self.has_diffable_solution():
|
162
|
+
return
|
163
|
+
if not self.side_by_side:
|
164
|
+
return
|
165
|
+
widget = self.query_one('#test-output', TwoSidedTestBoxWidget)
|
166
|
+
widget.diff_with_data = diff_with_data
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
from textual.app import ComposeResult
|
4
|
+
from textual.screen import ModalScreen
|
5
|
+
from textual.widgets import ListItem, ListView
|
6
|
+
|
7
|
+
|
8
|
+
class SelectorScreen(ModalScreen[int]):
|
9
|
+
BINDINGS = [('q', 'cancel', 'Cancel')]
|
10
|
+
|
11
|
+
def __init__(self, options: List[ListItem], title: Optional[str] = None):
|
12
|
+
super().__init__()
|
13
|
+
self.options = options
|
14
|
+
self.title = title
|
15
|
+
|
16
|
+
def compose(self) -> ComposeResult:
|
17
|
+
list_view = ListView(*self.options)
|
18
|
+
if self.title:
|
19
|
+
list_view.border_title = self.title
|
20
|
+
yield list_view
|
21
|
+
|
22
|
+
def on_list_view_selected(self, event: ListView.Selected):
|
23
|
+
self.dismiss(event.list_view.index)
|
24
|
+
|
25
|
+
def action_cancel(self):
|
26
|
+
self.dismiss(None)
|
@@ -5,18 +5,24 @@ from textual.containers import Horizontal, Vertical
|
|
5
5
|
from textual.screen import Screen
|
6
6
|
from textual.widgets import Footer, Header, Label, ListItem, ListView, RichLog
|
7
7
|
|
8
|
+
from rbx.box import package
|
9
|
+
from rbx.box.schema import TaskType
|
8
10
|
from rbx.box.testcase_extractors import (
|
9
11
|
GenerationTestcaseEntry,
|
10
12
|
extract_generation_testcases_from_groups,
|
11
13
|
)
|
12
14
|
from rbx.box.ui.widgets.file_log import FileLog
|
13
15
|
from rbx.box.ui.widgets.rich_log_box import RichLogBox
|
16
|
+
from rbx.box.ui.widgets.test_output_box import TestBoxWidget, TestcaseRenderingData
|
14
17
|
|
15
18
|
|
16
19
|
class TestExplorerScreen(Screen):
|
17
20
|
BINDINGS = [
|
18
21
|
('q', 'app.pop_screen', 'Quit'),
|
19
22
|
('m', 'toggle_metadata', 'Toggle metadata'),
|
23
|
+
('1', 'show_output', 'Show output'),
|
24
|
+
('2', 'show_stderr', 'Show stderr'),
|
25
|
+
('3', 'show_log', 'Show log'),
|
20
26
|
]
|
21
27
|
|
22
28
|
def __init__(self):
|
@@ -31,13 +37,15 @@ class TestExplorerScreen(Screen):
|
|
31
37
|
yield ListView(id='test-list')
|
32
38
|
with Vertical(id='test-details'):
|
33
39
|
yield FileLog(id='test-input')
|
34
|
-
yield
|
40
|
+
yield TestBoxWidget(id='test-output')
|
35
41
|
yield RichLogBox(id='test-metadata')
|
36
42
|
|
37
43
|
async def on_mount(self):
|
38
44
|
self.query_one('#test-list').border_title = 'Tests'
|
39
45
|
self.query_one('#test-input').border_title = 'Input'
|
40
|
-
|
46
|
+
|
47
|
+
# Ensure either output or interaction is visible.
|
48
|
+
self.action_show_output()
|
41
49
|
|
42
50
|
metadata = self.query_one('#test-metadata', RichLogBox)
|
43
51
|
metadata.display = False
|
@@ -53,17 +61,21 @@ class TestExplorerScreen(Screen):
|
|
53
61
|
|
54
62
|
def _update_selected_test(self, index: Optional[int]):
|
55
63
|
input = self.query_one('#test-input', FileLog)
|
56
|
-
output = self.query_one('#test-output',
|
64
|
+
output = self.query_one('#test-output', TestBoxWidget)
|
57
65
|
metadata = self.query_one('#test-metadata', RichLog)
|
58
66
|
|
59
67
|
if index is None:
|
60
68
|
input.path = None
|
61
|
-
output.
|
69
|
+
output.reset()
|
62
70
|
metadata.clear().write('No test selected')
|
63
71
|
return
|
64
72
|
entry = self._entries[index]
|
65
73
|
input.path = entry.metadata.copied_to.inputPath
|
66
|
-
|
74
|
+
|
75
|
+
assert entry.metadata.copied_to.outputPath is not None
|
76
|
+
output.data = TestcaseRenderingData.from_one_path(
|
77
|
+
entry.metadata.copied_to.outputPath
|
78
|
+
)
|
67
79
|
|
68
80
|
metadata.clear()
|
69
81
|
metadata.write(
|
@@ -98,3 +110,19 @@ class TestExplorerScreen(Screen):
|
|
98
110
|
await self.query_one('#test-list', ListView).extend(
|
99
111
|
[ListItem(Label(name)) for name in test_names]
|
100
112
|
)
|
113
|
+
|
114
|
+
def is_interactive(self) -> bool:
|
115
|
+
pkg = package.find_problem_package_or_die()
|
116
|
+
return pkg.type == TaskType.COMMUNICATION
|
117
|
+
|
118
|
+
def action_show_output(self):
|
119
|
+
if self.is_interactive():
|
120
|
+
self.query_one('#test-output', TestBoxWidget).show_interaction()
|
121
|
+
else:
|
122
|
+
self.query_one('#test-output', TestBoxWidget).show_output()
|
123
|
+
|
124
|
+
def action_show_stderr(self):
|
125
|
+
self.query_one('#test-output', TestBoxWidget).show_stderr()
|
126
|
+
|
127
|
+
def action_show_log(self):
|
128
|
+
self.query_one('#test-output', TestBoxWidget).show_log()
|
File without changes
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import typing
|
2
|
+
from typing import List, Optional
|
3
|
+
|
4
|
+
from rbx import utils
|
5
|
+
from rbx.box import package, solutions
|
6
|
+
from rbx.box.solutions import SolutionReportSkeleton, SolutionSkeleton
|
7
|
+
from rbx.box.testcase_utils import TestcaseEntry
|
8
|
+
from rbx.grading.steps import Evaluation
|
9
|
+
|
10
|
+
|
11
|
+
def has_run() -> bool:
|
12
|
+
return (package.get_problem_runs_dir() / 'skeleton.yml').is_file()
|
13
|
+
|
14
|
+
|
15
|
+
def get_skeleton() -> SolutionReportSkeleton:
|
16
|
+
skeleton_path = package.get_problem_runs_dir() / 'skeleton.yml'
|
17
|
+
return utils.model_from_yaml(
|
18
|
+
SolutionReportSkeleton,
|
19
|
+
skeleton_path.read_text(),
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
def get_solution_eval(
|
24
|
+
solution: SolutionSkeleton, entry: TestcaseEntry
|
25
|
+
) -> Optional[Evaluation]:
|
26
|
+
path = solution.get_entry_prefix(entry).with_suffix('.eval')
|
27
|
+
if not path.is_file():
|
28
|
+
return None
|
29
|
+
return utils.model_from_yaml(Evaluation, path.read_text())
|
30
|
+
|
31
|
+
|
32
|
+
def get_solution_evals(
|
33
|
+
skeleton: SolutionReportSkeleton, solution: SolutionSkeleton
|
34
|
+
) -> List[Optional[Evaluation]]:
|
35
|
+
return [get_solution_eval(solution, entry) for entry in skeleton.entries]
|
36
|
+
|
37
|
+
|
38
|
+
def get_solution_evals_or_null(
|
39
|
+
skeleton: SolutionReportSkeleton, solution: SolutionSkeleton
|
40
|
+
) -> Optional[List[Evaluation]]:
|
41
|
+
evals = get_solution_evals(skeleton, solution)
|
42
|
+
if any(e is None for e in evals):
|
43
|
+
return None
|
44
|
+
return typing.cast(List[Evaluation], evals)
|
45
|
+
|
46
|
+
|
47
|
+
def get_solution_markup(
|
48
|
+
skeleton: SolutionReportSkeleton, solution: SolutionSkeleton
|
49
|
+
) -> str:
|
50
|
+
header = f'[b $accent]{solution.path}[/b $accent] ({solution.path})'
|
51
|
+
|
52
|
+
evals = get_solution_evals_or_null(skeleton, solution)
|
53
|
+
report = solutions.get_solution_outcome_report(
|
54
|
+
solution, evals or [], skeleton.verification, subset=False
|
55
|
+
)
|
56
|
+
if evals is None:
|
57
|
+
return header + '\n' + report.get_verdict_markup(incomplete=True)
|
58
|
+
return header + '\n' + report.get_outcome_markup()
|
59
|
+
|
60
|
+
|
61
|
+
def get_run_testcase_markup(solution: SolutionSkeleton, entry: TestcaseEntry) -> str:
|
62
|
+
eval = get_solution_eval(solution, entry)
|
63
|
+
if eval is None:
|
64
|
+
return f'{entry}'
|
65
|
+
testcase_markup = solutions.get_testcase_markup_verdict(eval)
|
66
|
+
return f'{testcase_markup} {entry}'
|
67
|
+
|
68
|
+
|
69
|
+
def _get_checker_msg_last_line(eval: Evaluation) -> Optional[str]:
|
70
|
+
if eval.result.message is None:
|
71
|
+
return None
|
72
|
+
return eval.result.message.rstrip().split('\n')[-1]
|
73
|
+
|
74
|
+
|
75
|
+
def get_run_testcase_metadata_markup(
|
76
|
+
skeleton: SolutionReportSkeleton, solution: SolutionSkeleton, entry: TestcaseEntry
|
77
|
+
) -> Optional[str]:
|
78
|
+
eval = get_solution_eval(solution, entry)
|
79
|
+
if eval is None:
|
80
|
+
return None
|
81
|
+
time_str = solutions.get_capped_evals_formatted_time(
|
82
|
+
solution, [eval], skeleton.verification
|
83
|
+
)
|
84
|
+
memory_str = solutions.get_evals_formatted_memory([eval])
|
85
|
+
|
86
|
+
checker_msg = _get_checker_msg_last_line(eval)
|
87
|
+
|
88
|
+
lines = []
|
89
|
+
lines.append(
|
90
|
+
f'[b]{solutions.get_full_outcome_markup_verdict(eval.result.outcome)}[/b]'
|
91
|
+
)
|
92
|
+
lines.append(f'[b]Time:[/b] {time_str} / [b]Memory:[/b] {memory_str}')
|
93
|
+
if checker_msg is not None:
|
94
|
+
lines.append(f'[b]Checker:[/b] {checker_msg}')
|
95
|
+
return '\n'.join(lines)
|
File without changes
|
rbx/box/ui/widgets/file_log.py
CHANGED
@@ -54,10 +54,12 @@ class FileLog(Widget, can_focus=False):
|
|
54
54
|
log.clear()
|
55
55
|
|
56
56
|
if path is None:
|
57
|
+
self.border_subtitle = '(no file selected)'
|
57
58
|
return
|
58
59
|
|
59
60
|
if not path.is_file():
|
60
|
-
|
61
|
+
path_str = str(path.relative_to(pathlib.Path.cwd()))
|
62
|
+
self.border_subtitle = f'{path_str} (does not exist)'
|
61
63
|
return
|
62
64
|
|
63
65
|
self._load_file(path)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import asyncio
|
2
|
+
import pathlib
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
import rich.text
|
6
|
+
from textual import work
|
7
|
+
from textual.reactive import reactive
|
8
|
+
|
9
|
+
from rbx.box import testcase_utils
|
10
|
+
from rbx.box.ui.widgets.rich_log_box import RichLogBox
|
11
|
+
|
12
|
+
BATCH_SIZE = 1024
|
13
|
+
|
14
|
+
|
15
|
+
class InteractionBox(RichLogBox):
|
16
|
+
DEFAULT_CSS = """
|
17
|
+
InteractionBox {
|
18
|
+
border: solid $accent;
|
19
|
+
height: 1fr;
|
20
|
+
width: 1fr;
|
21
|
+
}
|
22
|
+
"""
|
23
|
+
|
24
|
+
path: reactive[Optional[pathlib.Path]] = reactive(None)
|
25
|
+
|
26
|
+
def on_mount(self):
|
27
|
+
super().on_mount()
|
28
|
+
self.auto_scroll = False
|
29
|
+
self.can_focus = False
|
30
|
+
|
31
|
+
@work(exclusive=True)
|
32
|
+
async def _load_file(self, path: pathlib.Path):
|
33
|
+
self.clear()
|
34
|
+
path_str = str(path.relative_to(pathlib.Path.cwd()))
|
35
|
+
self.border_subtitle = f'{path_str} (loading...)'
|
36
|
+
|
37
|
+
interaction = await asyncio.to_thread(testcase_utils.parse_interaction, path)
|
38
|
+
|
39
|
+
for entry in interaction.entries:
|
40
|
+
if entry.pipe == 0:
|
41
|
+
self.write(rich.text.Text(entry.data.rstrip(), style='green'))
|
42
|
+
else:
|
43
|
+
self.write(rich.text.Text(entry.data.rstrip()))
|
44
|
+
|
45
|
+
self.border_subtitle = path_str
|
46
|
+
|
47
|
+
async def watch_path(self, path: Optional[pathlib.Path]):
|
48
|
+
self.clear()
|
49
|
+
|
50
|
+
if path is None:
|
51
|
+
self.border_subtitle = '(no file selected)'
|
52
|
+
return
|
53
|
+
|
54
|
+
if not path.is_file():
|
55
|
+
path_str = str(path.relative_to(pathlib.Path.cwd()))
|
56
|
+
self.border_subtitle = f'{path_str} (does not exist)'
|
57
|
+
return
|
58
|
+
|
59
|
+
self._load_file(path)
|