rbx.cp 0.5.61__py3-none-any.whl → 0.5.62__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 +238 -113
- 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 +155 -0
- rbx/box/ui/screens/selector.py +26 -0
- rbx/box/ui/screens/test_explorer.py +20 -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/test_output_box.py +104 -0
- rbx/box/ui/widgets/two_sided_test_output_box.py +56 -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.62.dist-info}/METADATA +1 -1
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/RECORD +39 -30
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.62.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,155 @@
|
|
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.solutions import SolutionReportSkeleton, SolutionSkeleton
|
11
|
+
from rbx.box.testcase_extractors import (
|
12
|
+
GenerationTestcaseEntry,
|
13
|
+
extract_generation_testcases,
|
14
|
+
)
|
15
|
+
from rbx.box.ui.utils.run_ui import (
|
16
|
+
get_run_testcase_markup,
|
17
|
+
get_run_testcase_metadata_markup,
|
18
|
+
)
|
19
|
+
from rbx.box.ui.widgets.file_log import FileLog
|
20
|
+
from rbx.box.ui.widgets.test_output_box import TestcaseRenderingData
|
21
|
+
from rbx.box.ui.widgets.two_sided_test_output_box import TwoSidedTestBoxWidget
|
22
|
+
|
23
|
+
|
24
|
+
class RunTestExplorerScreen(Screen):
|
25
|
+
BINDINGS = [
|
26
|
+
('q', 'app.pop_screen', 'Quit'),
|
27
|
+
('1', 'show_output', 'Show output'),
|
28
|
+
('2', 'show_stderr', 'Show stderr'),
|
29
|
+
('3', 'show_log', 'Show log'),
|
30
|
+
('m', 'toggle_metadata', 'Toggle metadata'),
|
31
|
+
('s', 'toggle_side_by_side', 'Toggle sxs'),
|
32
|
+
]
|
33
|
+
|
34
|
+
side_by_side: reactive[bool] = reactive(False)
|
35
|
+
diff_with_data: reactive[Optional[TestcaseRenderingData]] = reactive(
|
36
|
+
default=None,
|
37
|
+
)
|
38
|
+
|
39
|
+
def __init__(
|
40
|
+
self,
|
41
|
+
skeleton: SolutionReportSkeleton,
|
42
|
+
solution: SolutionSkeleton,
|
43
|
+
diff_solution: Optional[SolutionSkeleton] = None,
|
44
|
+
):
|
45
|
+
super().__init__()
|
46
|
+
self.skeleton = skeleton
|
47
|
+
self.solution = solution
|
48
|
+
self.diff_solution = diff_solution
|
49
|
+
self.set_reactive(RunTestExplorerScreen.side_by_side, diff_solution is not None)
|
50
|
+
|
51
|
+
self._entries: List[GenerationTestcaseEntry] = []
|
52
|
+
|
53
|
+
def compose(self) -> ComposeResult:
|
54
|
+
yield Header()
|
55
|
+
yield Footer()
|
56
|
+
with Horizontal(id='test-explorer'):
|
57
|
+
with Vertical(id='test-list-container'):
|
58
|
+
yield ListView(id='test-list')
|
59
|
+
with Vertical(id='test-details'):
|
60
|
+
yield FileLog(id='test-input')
|
61
|
+
yield TwoSidedTestBoxWidget(id='test-output')
|
62
|
+
|
63
|
+
async def on_mount(self):
|
64
|
+
self.query_one('#test-list').border_title = 'Tests'
|
65
|
+
self.query_one('#test-input').border_title = 'Input'
|
66
|
+
|
67
|
+
await self._update_tests()
|
68
|
+
|
69
|
+
def _get_rendering_data(
|
70
|
+
self, solution: SolutionSkeleton, entry: GenerationTestcaseEntry
|
71
|
+
) -> TestcaseRenderingData:
|
72
|
+
rendering_data = TestcaseRenderingData.from_one_path(
|
73
|
+
solution.get_entry_prefix(entry.group_entry)
|
74
|
+
)
|
75
|
+
rendering_data.rich_content = get_run_testcase_metadata_markup(
|
76
|
+
self.skeleton, solution, entry.group_entry
|
77
|
+
)
|
78
|
+
return rendering_data
|
79
|
+
|
80
|
+
def _update_selected_test(self, index: Optional[int]):
|
81
|
+
input = self.query_one('#test-input', FileLog)
|
82
|
+
output = self.query_one('#test-output', TwoSidedTestBoxWidget)
|
83
|
+
|
84
|
+
if index is None:
|
85
|
+
input.path = None
|
86
|
+
output.reset()
|
87
|
+
return
|
88
|
+
entry = self._entries[index]
|
89
|
+
input.path = entry.metadata.copied_to.inputPath
|
90
|
+
output.data = self._get_rendering_data(self.solution, entry)
|
91
|
+
|
92
|
+
if self.diff_solution is not None:
|
93
|
+
self.diff_with_data = self._get_rendering_data(self.diff_solution, entry)
|
94
|
+
else:
|
95
|
+
self.diff_with_data = TestcaseRenderingData.from_one_path(
|
96
|
+
entry.group_entry.get_prefix_path()
|
97
|
+
)
|
98
|
+
|
99
|
+
async def _update_tests(self):
|
100
|
+
self.watch(
|
101
|
+
self.query_one('#test-list', ListView),
|
102
|
+
'index',
|
103
|
+
self._update_selected_test,
|
104
|
+
)
|
105
|
+
|
106
|
+
self._entries = await extract_generation_testcases(self.skeleton.entries)
|
107
|
+
|
108
|
+
test_markups = [
|
109
|
+
get_run_testcase_markup(self.solution, entry.group_entry)
|
110
|
+
for entry in self._entries
|
111
|
+
]
|
112
|
+
|
113
|
+
await self.query_one('#test-list', ListView).clear()
|
114
|
+
await self.query_one('#test-list', ListView).extend(
|
115
|
+
[ListItem(Label(name, markup=True)) for name in test_markups]
|
116
|
+
)
|
117
|
+
|
118
|
+
def has_diffable_solution(self) -> bool:
|
119
|
+
return self.diff_solution is not None or package.get_main_solution() is not None
|
120
|
+
|
121
|
+
def action_show_output(self):
|
122
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).show_output()
|
123
|
+
|
124
|
+
def action_show_stderr(self):
|
125
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).show_stderr()
|
126
|
+
|
127
|
+
def action_show_log(self):
|
128
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).show_log()
|
129
|
+
|
130
|
+
def action_toggle_metadata(self):
|
131
|
+
self.query_one('#test-output', TwoSidedTestBoxWidget).toggle_metadata()
|
132
|
+
|
133
|
+
def action_toggle_side_by_side(self):
|
134
|
+
self.side_by_side = not self.side_by_side
|
135
|
+
|
136
|
+
def watch_side_by_side(self, side_by_side: bool):
|
137
|
+
widget = self.query_one('#test-output', TwoSidedTestBoxWidget)
|
138
|
+
|
139
|
+
if side_by_side:
|
140
|
+
if not self.has_diffable_solution():
|
141
|
+
self.app.notify(
|
142
|
+
'Found no solution to compare against', severity='error'
|
143
|
+
)
|
144
|
+
return
|
145
|
+
widget.diff_with_data = self.diff_with_data
|
146
|
+
else:
|
147
|
+
widget.diff_with_data = None
|
148
|
+
|
149
|
+
def watch_diff_with_data(self, diff_with_data: Optional[TestcaseRenderingData]):
|
150
|
+
if not self.has_diffable_solution():
|
151
|
+
return
|
152
|
+
if not self.side_by_side:
|
153
|
+
return
|
154
|
+
widget = self.query_one('#test-output', TwoSidedTestBoxWidget)
|
155
|
+
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)
|
@@ -11,12 +11,16 @@ from rbx.box.testcase_extractors import (
|
|
11
11
|
)
|
12
12
|
from rbx.box.ui.widgets.file_log import FileLog
|
13
13
|
from rbx.box.ui.widgets.rich_log_box import RichLogBox
|
14
|
+
from rbx.box.ui.widgets.test_output_box import TestBoxWidget, TestcaseRenderingData
|
14
15
|
|
15
16
|
|
16
17
|
class TestExplorerScreen(Screen):
|
17
18
|
BINDINGS = [
|
18
19
|
('q', 'app.pop_screen', 'Quit'),
|
19
20
|
('m', 'toggle_metadata', 'Toggle metadata'),
|
21
|
+
('1', 'show_output', 'Show output'),
|
22
|
+
('2', 'show_stderr', 'Show stderr'),
|
23
|
+
('3', 'show_log', 'Show log'),
|
20
24
|
]
|
21
25
|
|
22
26
|
def __init__(self):
|
@@ -31,13 +35,12 @@ class TestExplorerScreen(Screen):
|
|
31
35
|
yield ListView(id='test-list')
|
32
36
|
with Vertical(id='test-details'):
|
33
37
|
yield FileLog(id='test-input')
|
34
|
-
yield
|
38
|
+
yield TestBoxWidget(id='test-output')
|
35
39
|
yield RichLogBox(id='test-metadata')
|
36
40
|
|
37
41
|
async def on_mount(self):
|
38
42
|
self.query_one('#test-list').border_title = 'Tests'
|
39
43
|
self.query_one('#test-input').border_title = 'Input'
|
40
|
-
self.query_one('#test-output').border_title = 'Output'
|
41
44
|
|
42
45
|
metadata = self.query_one('#test-metadata', RichLogBox)
|
43
46
|
metadata.display = False
|
@@ -53,17 +56,20 @@ class TestExplorerScreen(Screen):
|
|
53
56
|
|
54
57
|
def _update_selected_test(self, index: Optional[int]):
|
55
58
|
input = self.query_one('#test-input', FileLog)
|
56
|
-
output = self.query_one('#test-output',
|
59
|
+
output = self.query_one('#test-output', TestBoxWidget)
|
57
60
|
metadata = self.query_one('#test-metadata', RichLog)
|
58
61
|
|
59
62
|
if index is None:
|
60
63
|
input.path = None
|
61
|
-
output.
|
64
|
+
output.reset()
|
62
65
|
metadata.clear().write('No test selected')
|
63
66
|
return
|
64
67
|
entry = self._entries[index]
|
65
68
|
input.path = entry.metadata.copied_to.inputPath
|
66
|
-
|
69
|
+
|
70
|
+
output.data = TestcaseRenderingData.from_one_path(
|
71
|
+
entry.metadata.copied_to.outputPath
|
72
|
+
)
|
67
73
|
|
68
74
|
metadata.clear()
|
69
75
|
metadata.write(
|
@@ -98,3 +104,12 @@ class TestExplorerScreen(Screen):
|
|
98
104
|
await self.query_one('#test-list', ListView).extend(
|
99
105
|
[ListItem(Label(name)) for name in test_names]
|
100
106
|
)
|
107
|
+
|
108
|
+
def action_show_output(self):
|
109
|
+
self.query_one('#test-output', TestBoxWidget).show_output()
|
110
|
+
|
111
|
+
def action_show_stderr(self):
|
112
|
+
self.query_one('#test-output', TestBoxWidget).show_stderr()
|
113
|
+
|
114
|
+
def action_show_log(self):
|
115
|
+
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,104 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import pathlib
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from textual.app import ComposeResult
|
6
|
+
from textual.containers import Vertical
|
7
|
+
from textual.reactive import reactive
|
8
|
+
from textual.widget import Widget
|
9
|
+
from textual.widgets import ContentSwitcher
|
10
|
+
|
11
|
+
from rbx.box.ui.widgets.file_log import FileLog
|
12
|
+
from rbx.box.ui.widgets.rich_log_box import RichLogBox
|
13
|
+
|
14
|
+
|
15
|
+
@dataclasses.dataclass
|
16
|
+
class TestcaseRenderingData:
|
17
|
+
input_path: Optional[pathlib.Path] = None
|
18
|
+
output_path: Optional[pathlib.Path] = None
|
19
|
+
stderr_path: Optional[pathlib.Path] = None
|
20
|
+
log_path: Optional[pathlib.Path] = None
|
21
|
+
rich_content: Optional[str] = None
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def from_one_path(cls, path: pathlib.Path) -> 'TestcaseRenderingData':
|
25
|
+
return cls(
|
26
|
+
input_path=path.with_suffix('.in'),
|
27
|
+
output_path=path.with_suffix('.out'),
|
28
|
+
stderr_path=path.with_suffix('.err'),
|
29
|
+
log_path=path.with_suffix('.log'),
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
class TestBoxWidget(Widget, can_focus=False):
|
34
|
+
data: reactive[TestcaseRenderingData] = reactive(
|
35
|
+
default=lambda: TestcaseRenderingData(),
|
36
|
+
bindings=True,
|
37
|
+
)
|
38
|
+
|
39
|
+
@dataclasses.dataclass
|
40
|
+
class Logs:
|
41
|
+
output: FileLog
|
42
|
+
stderr: FileLog
|
43
|
+
log: FileLog
|
44
|
+
|
45
|
+
def logs(self) -> Logs:
|
46
|
+
return self.Logs(
|
47
|
+
output=self.query_one('#test-box-output', FileLog),
|
48
|
+
stderr=self.query_one('#test-box-stderr', FileLog),
|
49
|
+
log=self.query_one('#test-box-log', FileLog),
|
50
|
+
)
|
51
|
+
|
52
|
+
def compose(self) -> ComposeResult:
|
53
|
+
with Vertical():
|
54
|
+
with ContentSwitcher(initial='test-box-output', id='test-box-switcher'):
|
55
|
+
yield FileLog(id='test-box-output')
|
56
|
+
yield FileLog(id='test-box-stderr')
|
57
|
+
yield FileLog(id='test-box-log')
|
58
|
+
yield RichLogBox(id='test-box-metadata')
|
59
|
+
|
60
|
+
def on_mount(self):
|
61
|
+
logs = self.logs()
|
62
|
+
logs.output.border_title = 'Output'
|
63
|
+
logs.stderr.border_title = 'Stderr'
|
64
|
+
logs.log.border_title = 'Log'
|
65
|
+
|
66
|
+
metadata = self.query_one('#test-box-metadata', RichLogBox)
|
67
|
+
metadata.display = False
|
68
|
+
metadata.border_title = 'Metadata'
|
69
|
+
metadata.wrap = True
|
70
|
+
metadata.markup = True
|
71
|
+
metadata.clear()
|
72
|
+
metadata.write('No metadata')
|
73
|
+
|
74
|
+
self.watch_data(self.data)
|
75
|
+
|
76
|
+
def watch_data(self, data: TestcaseRenderingData):
|
77
|
+
logs = self.logs()
|
78
|
+
logs.output.path = data.output_path
|
79
|
+
logs.stderr.path = data.stderr_path
|
80
|
+
logs.log.path = data.log_path
|
81
|
+
|
82
|
+
metadata = self.query_one('#test-box-metadata', RichLogBox)
|
83
|
+
metadata.clear()
|
84
|
+
if data.rich_content is not None:
|
85
|
+
metadata.write(data.rich_content)
|
86
|
+
else:
|
87
|
+
metadata.write('No metadata')
|
88
|
+
|
89
|
+
def reset(self):
|
90
|
+
self.data = TestcaseRenderingData()
|
91
|
+
|
92
|
+
def show_output(self):
|
93
|
+
self.query_one(ContentSwitcher).current = 'test-box-output'
|
94
|
+
|
95
|
+
def show_stderr(self):
|
96
|
+
self.query_one(ContentSwitcher).current = 'test-box-stderr'
|
97
|
+
|
98
|
+
def show_log(self):
|
99
|
+
self.query_one(ContentSwitcher).current = 'test-box-log'
|
100
|
+
|
101
|
+
def toggle_metadata(self):
|
102
|
+
metadata = self.query_one('#test-box-metadata', RichLogBox)
|
103
|
+
metadata.display = not metadata.display
|
104
|
+
metadata.refresh()
|