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.
Files changed (40) hide show
  1. rbx/box/cd.py +14 -0
  2. rbx/box/cli.py +6 -0
  3. rbx/box/code.py +34 -5
  4. rbx/box/contest/main.py +6 -2
  5. rbx/box/git_utils.py +28 -0
  6. rbx/box/package.py +23 -0
  7. rbx/box/packaging/boca/packager.py +3 -18
  8. rbx/box/packaging/moj/packager.py +1 -1
  9. rbx/box/packaging/polygon/upload.py +7 -5
  10. rbx/box/presets/__init__.py +80 -6
  11. rbx/box/presets/fetch.py +18 -1
  12. rbx/box/retries.py +2 -0
  13. rbx/box/solutions.py +242 -114
  14. rbx/box/solutions_test.py +3 -1
  15. rbx/box/tasks.py +6 -1
  16. rbx/box/testcase_utils.py +3 -0
  17. rbx/box/ui/css/app.tcss +14 -2
  18. rbx/box/ui/main.py +3 -5
  19. rbx/box/ui/screens/error.py +19 -0
  20. rbx/box/ui/screens/run.py +4 -12
  21. rbx/box/ui/screens/run_explorer.py +77 -1
  22. rbx/box/ui/screens/run_test_explorer.py +166 -0
  23. rbx/box/ui/screens/selector.py +26 -0
  24. rbx/box/ui/screens/test_explorer.py +33 -5
  25. rbx/box/ui/utils/__init__.py +0 -0
  26. rbx/box/ui/utils/run_ui.py +95 -0
  27. rbx/box/ui/widgets/__init__.py +0 -0
  28. rbx/box/ui/widgets/file_log.py +3 -1
  29. rbx/box/ui/widgets/interaction_box.py +59 -0
  30. rbx/box/ui/widgets/test_output_box.py +113 -0
  31. rbx/box/ui/widgets/two_sided_test_output_box.py +60 -0
  32. rbx/grading/steps.py +1 -0
  33. rbx/resources/packagers/boca/compile/java +55 -59
  34. rbx/resources/packagers/boca/interactive/java +2 -2
  35. rbx/resources/packagers/boca/run/java +2 -2
  36. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/METADATA +1 -1
  37. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/RECORD +40 -30
  38. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/LICENSE +0 -0
  39. {rbx_cp-0.5.61.dist-info → rbx_cp-0.5.63.dist-info}/WHEEL +0 -0
  40. {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-metadata {
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.build import BuildScreen
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
- ('Run solutions against define testsets.', RunScreen),
14
- ('Build tests.', BuildScreen),
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
- pkg = package.find_problem_package_or_die()
92
- sol_idx_in_skeleton = self._find_solution_index_in_skeleton(
93
- pkg.solutions[item.solution_index]
94
- )
95
- group = self.skeleton.find_group_skeleton(item.group_name)
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.testcase_index]
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
- pass
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 FileLog(id='test-output')
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
- self.query_one('#test-output').border_title = 'Output'
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', FileLog)
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.path = None
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
- output.path = entry.metadata.copied_to.outputPath
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
@@ -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
- self.query_one(Log).write(f'File {path} does not exist')
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)