Merge lp:~sylvain-pineau/checkbox/fixes into lp:checkbox
- fixes
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 2719 |
Merged at revision: | 2713 |
Proposed branch: | lp:~sylvain-pineau/checkbox/fixes |
Merge into: | lp:checkbox |
Diff against target: |
802 lines (+425/-231) 1 file modified
checkbox-ng/checkbox_ng/commands/cli.py (+425/-231) |
To merge this branch: | bzr merge lp:~sylvain-pineau/checkbox/fixes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Zygmunt Krynicki (community) | Approve | ||
Review via email: mp+208082@code.launchpad.net |
Commit message
Description of the change
Add Textland versions of curses widgets and a test selection screen for checkbox-ng cli tools.
As checkbox-gui it pre-run all local jobs to build the list of categories.
The test selection screen window is scrollable with the mouse (without clipping), categories can be collapsed/expanded, selection of a test automatically refresh ancestors and children tests.
Zygmunt Krynicki (zyga) wrote : | # |
Sylvain Pineau (sylvain-pineau) wrote : | # |
Done, manager replaced the confusing word session;
Daniel Manrique (roadmr) wrote : | # |
The attempt to merge lp:~sylvain-pineau/checkbox/fixes into lp:checkbox failed. Below is the output from the failed tests.
[precise] Bringing VM 'up'
[precise] (timing) 7.96user 3.49system 4:07.83elapsed 4%CPU (0avgtext+0avgdata 21524maxresident)k
[precise] (timing) 0inputs+192outputs (0major+
[precise] Starting tests...
[precise] Checkbox GUI build: [32;1mPASS[39;0m
[precise] (timing) 0.86user 0.30system 1:03.08elapsed 1%CPU (0avgtext+0avgdata 19896maxresident)k
[precise] (timing) 0inputs+64outputs (0major+
[precise] CheckBox test suite: [32;1mPASS[39;0m
[precise] (timing) 0.84user 0.33system 0:38.11elapsed 3%CPU (0avgtext+0avgdata 20080maxresident)k
[precise] (timing) 0inputs+40outputs (0major+
[precise] (timing) 0.80user 0.39system 0:05.99elapsed 19%CPU (0avgtext+0avgdata 20560maxresident)k
[precise] (timing) 0inputs+16outputs (0major+
[precise] PlainBox test suite: [32;1mPASS[39;0m
[precise] (timing) 1.19user 0.35system 0:18.69elapsed 8%CPU (0avgtext+0avgdata 20188maxresident)k
[precise] (timing) 0inputs+344outputs (0major+
[precise] PlainBox documentation build: [32;1mPASS[39;0m
[precise] (timing) 0.93user 0.37system 0:32.60elapsed 4%CPU (0avgtext+0avgdata 20780maxresident)k
[precise] (timing) 0inputs+32outputs (0major+
[precise] CheckBoxNG test suite: [31;1mFAIL[39;0m
[precise] stdout: http://
[precise] stderr: http://
[precise] (timing) Command exited with non-zero status 1
[precise] (timing) 0.78user 0.34system 0:06.92elapsed 16%CPU (0avgtext+0avgdata 20016maxresident)k
[precise] (timing) 0inputs+24outputs (0major+
[precise] Integration tests: [32;1mPASS[39;0m
[precise] (timing) 0.79user 0.34system 0:09.49elapsed 11%CPU (0avgtext+0avgdata 19992maxresident)k
[precise] (timing) 0inputs+8outputs (0major+
[precise] Destroying VM
[trusty] Bringing VM 'up'
[trusty] (timing) 10.49user 4.64system 5:06.78elapsed 4%CPU (0avgtext+0avgdata 21304maxresident)k
[trusty] (timing) 0inputs+248outputs (0major+
[trusty] Starting tests...
[trusty] Checkbox GUI build: [32;1mPASS[39;0m
[trusty] (timing) 1.22user 0.44system 1:08.98elapsed 2%CPU (0avgtext+0avgdata 20800maxresident)k
[trusty] (timing) 5944inputs+
[trusty] CheckBox test suite: [32;1mPASS[39;0m
[trusty] (timing) 0.94user 0.36system 0:42.36elapsed 3%CPU (0avgtext+0avgdata 20652maxresident)k
[trusty] (timing) 0inputs+48outputs (0major+
[trusty] (timing) 0.86user 0.30system 0:07.87elapsed 14%CPU (0avgtext+0avgdata 20636maxresident)k
[trusty] (timing) 0inputs+16outputs (0major+
[trusty] PlainBox test suite: [32;1mPASS[39;0m
[trusty] (timing) 1.15user 0.40system 0:21.03elapsed 7%CPU (0avgtext+0avgdata 20504maxresident)k
[trusty] (timing) 0inputs+336outputs (0major+
[trusty] PlainBox documentation build: [32;1mPASS[39;0m
[trusty] (timing) 0.84user 0.35sy...
- 2709. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Rely on Textland to display the first screens The Welcome screen and the suite selection menu.
textland
- 2710. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Remove the curses versions of the first screens The Welcome screen and the suite selection menu
- 2711. By Sylvain Pineau
-
checkbox_
ng:commands: cli: New ScrollableTreeNode class Class used to display/interact with a SelectableJobTr
eeNode. the Key mapping is quite simple:
UP/DOWN: Scroll the tree view
SPACE: Select a test
ENTER: Toggle expand/collapse
S: Select all
D: Deselect all
T: Start testing - 2712. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Use the new SessionManager API - 2713. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Reduce test execution verbosity (1/3) Avoid displaying information related to local or resource jobs.
- 2714. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Reduce test execution verbosity (2/3) Do not display empty comments.
- 2715. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Reduce test execution verbosity (3/3) Turn off the IO delegate printing all stdout/stderr of job commands.
- 2716. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Use select_job() to populate the desired job list - 2717. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Pre-run local jobs to create the tree view categories _run_jobs() is no longer responsible for starting the session, this code has
been moved to the run() method so that local job results are kept and the
session still opened before updating the desired job list with the result of
the TreeNode selection. - 2718. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Only print job description if the job can start - 2719. By Sylvain Pineau
-
checkbox_
ng:commands: cli: Print the unsupported outcome if the job cannot start
Sylvain Pineau (sylvain-pineau) wrote : | # |
Re-approving, the textland import paths have been fixed.
Sylvain Pineau (sylvain-pineau) wrote : | # |
I depend on https:/
just waiting for it to land and I'll re-approve the MR.
Preview Diff
1 | === modified file 'checkbox-ng/checkbox_ng/commands/cli.py' |
2 | --- checkbox-ng/checkbox_ng/commands/cli.py 2014-02-24 12:45:18 +0000 |
3 | +++ checkbox-ng/checkbox_ng/commands/cli.py 2014-02-25 14:19:59 +0000 |
4 | @@ -29,14 +29,13 @@ |
5 | from logging import getLogger |
6 | from os.path import join |
7 | from shutil import copyfileobj |
8 | -import curses |
9 | import io |
10 | import os |
11 | import sys |
12 | import textwrap |
13 | |
14 | from plainbox.abc import IJobResult |
15 | -from plainbox.impl.applogic import get_matching_job_list, get_whitelist_by_name |
16 | +from plainbox.impl.applogic import get_whitelist_by_name |
17 | from plainbox.impl.commands import PlainBoxCommand |
18 | from plainbox.impl.commands.check_config import CheckConfigInvocation |
19 | from plainbox.impl.commands.checkbox import CheckBoxCommandMixIn |
20 | @@ -52,8 +51,21 @@ |
21 | from plainbox.impl.runner import authenticate_warmup |
22 | from plainbox.impl.runner import slugify |
23 | from plainbox.impl.secure.config import Unset |
24 | +from plainbox.impl.secure.qualifiers import CompositeQualifier |
25 | +from plainbox.impl.secure.qualifiers import NonLocalJobQualifier |
26 | from plainbox.impl.secure.qualifiers import WhiteList |
27 | -from plainbox.impl.session import SessionStateLegacyAPI as SessionState |
28 | +from plainbox.impl.secure.qualifiers import select_jobs |
29 | +from plainbox.impl.session import SessionManager, SessionStorageRepository |
30 | +from plainbox.vendor.textland import DrawingContext |
31 | +from plainbox.vendor.textland import EVENT_KEYBOARD |
32 | +from plainbox.vendor.textland import EVENT_RESIZE |
33 | +from plainbox.vendor.textland import Event |
34 | +from plainbox.vendor.textland import IApplication |
35 | +from plainbox.vendor.textland import KeyboardData |
36 | +from plainbox.vendor.textland import Size |
37 | +from plainbox.vendor.textland import TextImage |
38 | +from plainbox.vendor.textland import get_display |
39 | +from plainbox.vendor.textland import NORMAL, REVERSE, UNDERLINE |
40 | |
41 | |
42 | logger = getLogger("checkbox.ng.commands.cli") |
43 | @@ -200,113 +212,243 @@ |
44 | category.set_descendants_state(new_state) |
45 | |
46 | |
47 | -def show_menu(stdscr, title, menu): |
48 | - """ |
49 | - Display the appropriate curses menu and return the selected options |
50 | - """ |
51 | - |
52 | - curses.use_default_colors() |
53 | - curses.curs_set(0) |
54 | - stdscr.keypad(1) |
55 | - # Modify color pair #1, to get black on white text |
56 | - curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) |
57 | - |
58 | - option_count = len(menu) |
59 | - position = 0 # Zero-based index of the selected menu option |
60 | - old_position = None |
61 | - key_pressed = None |
62 | - selection = [position] |
63 | - new_selection = False |
64 | - |
65 | - while True: |
66 | - if position != old_position or new_selection: |
67 | - stdscr.erase() |
68 | - old_position = position |
69 | - new_selection = False |
70 | - stdscr.border(0) |
71 | - # Display title at x=2, y=2 |
72 | - stdscr.addstr(2, 2, title, curses.A_STANDOUT) |
73 | - |
74 | - # Display all the menu items |
75 | - for i in range(option_count): |
76 | - text_style = curses.A_NORMAL |
77 | - if position == i: |
78 | - text_style = curses.color_pair(1) |
79 | - # Display options from line 4, column 4 |
80 | - stdscr.addstr(4 + i, 4, "[{}] - {}".format( |
81 | - 'X' if i in selection else ' ', |
82 | - menu[i].replace('ihv-', '').capitalize()), text_style) |
83 | - |
84 | - # Display "OK" at bottom of menu |
85 | - text_style = curses.A_NORMAL |
86 | - if position == option_count: |
87 | - text_style = curses.color_pair(1) |
88 | - # Add an empty line before the last option |
89 | - stdscr.addstr(5 + option_count, 4, "OK", text_style) |
90 | - stdscr.refresh() |
91 | - |
92 | - key_pressed = stdscr.getch() |
93 | - if key_pressed == curses.KEY_DOWN: |
94 | - if position < option_count: |
95 | - position += 1 |
96 | - else: |
97 | - position = 0 |
98 | - elif key_pressed == curses.KEY_UP: |
99 | - if position > 0: |
100 | - position -= 1 |
101 | - else: |
102 | - position = option_count |
103 | - elif position == option_count: |
104 | - break |
105 | - elif key_pressed == 32: # KEY_SPACE |
106 | - if position in selection: |
107 | - selection.remove(position) |
108 | - elif position < option_count: |
109 | - selection.append(position) |
110 | - new_selection = True |
111 | - |
112 | - return selection |
113 | - |
114 | - |
115 | -def show_welcome(stdscr, text): |
116 | - """ |
117 | - Display a curses splash screen containing a welcome text |
118 | - |
119 | - Left and right margins are set to 3 chars. Including the border width (1), |
120 | - the text is wrapped to screen width - 3 * 2 - 1 *2 using: |
121 | - * stdscr.getmaxyx()[1] - 8 |
122 | - * stdscr.addstr(i, 4, line) |
123 | - 8 equals to margins(3*2) + borders(2*1) |
124 | - 4 equals to left margin(3) + left border(1) |
125 | - """ |
126 | - curses.use_default_colors() |
127 | - curses.curs_set(0) |
128 | - stdscr.border(0) |
129 | - |
130 | - i = 0 |
131 | - for paragraph in text.splitlines(): |
132 | - i += 1 |
133 | - for line in textwrap.fill(paragraph, |
134 | - stdscr.getmaxyx()[1] - 8, |
135 | - replace_whitespace=False).splitlines(): |
136 | - stdscr.addstr(i, 4, line) |
137 | +class ShowWelcome(IApplication): |
138 | + """ |
139 | + Display a welcome message |
140 | + """ |
141 | + def __init__(self, text): |
142 | + self.image = TextImage(Size(0, 0)) |
143 | + self.text = text |
144 | + |
145 | + def consume_event(self, event: Event): |
146 | + if event.kind == EVENT_RESIZE: |
147 | + self.image = TextImage(event.data) # data is the new size |
148 | + elif event.kind == EVENT_KEYBOARD and event.data.key == "enter": |
149 | + raise StopIteration |
150 | + self.repaint(event) |
151 | + return self.image |
152 | + |
153 | + def repaint(self, event: Event): |
154 | + ctx = DrawingContext(self.image) |
155 | + i = 0 |
156 | + ctx.border() |
157 | + for paragraph in self.text.splitlines(): |
158 | i += 1 |
159 | - stdscr.addstr(i + 1, 4, "Continue", curses.A_STANDOUT) |
160 | - while True: |
161 | - key_pressed = stdscr.getch() |
162 | - if key_pressed == ord('\n'): |
163 | - break |
164 | + for line in textwrap.fill( |
165 | + paragraph, |
166 | + self.image.size.width - 8, |
167 | + replace_whitespace=False).splitlines(): |
168 | + ctx.move_to(4, i) |
169 | + ctx.print(line) |
170 | + i += 1 |
171 | + ctx.move_to(4, i + 1) |
172 | + ctx.attributes.style = REVERSE |
173 | + ctx.print("< Continue >") |
174 | + |
175 | + |
176 | +class ShowMenu(IApplication): |
177 | + """ |
178 | + Display the appropriate menu and return the selected options |
179 | + """ |
180 | + def __init__(self, title, menu): |
181 | + self.image = TextImage(Size(0, 0)) |
182 | + self.title = title |
183 | + self.menu = menu |
184 | + self.option_count = len(menu) |
185 | + self.position = 0 # Zero-based index of the selected menu option |
186 | + self.selection = [self.position] |
187 | + |
188 | + def consume_event(self, event: Event): |
189 | + if event.kind == EVENT_RESIZE: |
190 | + self.image = TextImage(event.data) # data is the new size |
191 | + elif event.kind == EVENT_KEYBOARD: |
192 | + if event.data.key == "down": |
193 | + if self.position < self.option_count: |
194 | + self.position += 1 |
195 | + else: |
196 | + self.position = 0 |
197 | + elif event.data.key == "up": |
198 | + if self.position > 0: |
199 | + self.position -= 1 |
200 | + else: |
201 | + self.position = self.option_count |
202 | + elif (event.data.key == "enter" and |
203 | + self.position == self.option_count): |
204 | + raise StopIteration(self.selection) |
205 | + elif event.data.key == "space": |
206 | + if self.position in self.selection: |
207 | + self.selection.remove(self.position) |
208 | + elif self.position < self.option_count: |
209 | + self.selection.append(self.position) |
210 | + self.repaint(event) |
211 | + return self.image |
212 | + |
213 | + def repaint(self, event: Event): |
214 | + ctx = DrawingContext(self.image) |
215 | + ctx.border(tm=1) |
216 | + ctx.attributes.style = REVERSE |
217 | + ctx.print(' ' * self.image.size.width) |
218 | + ctx.move_to(1, 0) |
219 | + ctx.print(self.title) |
220 | + |
221 | + # Display all the menu items |
222 | + for i in range(self.option_count): |
223 | + ctx.attributes.style = NORMAL |
224 | + if i == self.position: |
225 | + ctx.attributes.style = REVERSE |
226 | + # Display options from line 3, column 4 |
227 | + ctx.move_to(4, 3 + i) |
228 | + ctx.print("[{}] - {}".format( |
229 | + 'X' if i in self.selection else ' ', |
230 | + self.menu[i].replace('ihv-', '').capitalize())) |
231 | + |
232 | + # Display "OK" at bottom of menu |
233 | + ctx.attributes.style = NORMAL |
234 | + if self.position == self.option_count: |
235 | + ctx.attributes.style = REVERSE |
236 | + # Add an empty line before the last option |
237 | + ctx.move_to(4, 4 + self.option_count) |
238 | + ctx.print("< OK >") |
239 | + |
240 | + |
241 | +class ScrollableTreeNode(IApplication): |
242 | + """ |
243 | + Class used to interact with a SelectableJobTreeNode |
244 | + """ |
245 | + def __init__(self, tree, title): |
246 | + self.image = TextImage(Size(0, 0)) |
247 | + self.tree = tree |
248 | + self.title = title |
249 | + self.top = 0 # Top line number |
250 | + self.highlight = 0 # Highlighted line number |
251 | + |
252 | + def consume_event(self, event: Event): |
253 | + if event.kind == EVENT_RESIZE: |
254 | + self.image = TextImage(event.data) # data is the new size |
255 | + elif event.kind == EVENT_KEYBOARD: |
256 | + self.image = TextImage(self.image.size) |
257 | + if event.data.key == "up": |
258 | + self._scroll("up") |
259 | + elif event.data.key == "down": |
260 | + self._scroll("down") |
261 | + elif event.data.key == "space": |
262 | + self._selectNode() |
263 | + elif event.data.key == "enter": |
264 | + self._toggleNode() |
265 | + elif event.data.key in 'sS': |
266 | + self.tree.set_descendants_state(True) |
267 | + elif event.data.key in 'dD': |
268 | + self.tree.set_descendants_state(False) |
269 | + elif event.data.key in 'tT': |
270 | + raise StopIteration |
271 | + self.repaint(event) |
272 | + return self.image |
273 | + |
274 | + def repaint(self, event: Event): |
275 | + ctx = DrawingContext(self.image) |
276 | + ctx.border(tm=1, bm=1) |
277 | + cols = self.image.size.width |
278 | + extra_cols = 0 |
279 | + if cols > 80: |
280 | + extra_cols = cols - 80 |
281 | + ctx.attributes.style = REVERSE |
282 | + ctx.print(' ' * cols) |
283 | + ctx.move_to(1, 0) |
284 | + bottom = self.top + self.image.size.height - 4 |
285 | + ctx.print(self.title) |
286 | + ctx.move_to(1, self.image.size.height - 1) |
287 | + ctx.attributes.style = UNDERLINE |
288 | + ctx.print("Enter") |
289 | + ctx.move_to(6, self.image.size.height - 1) |
290 | + ctx.attributes.style = NORMAL |
291 | + ctx.print(": Expand/Collapse") |
292 | + ctx.move_to(27, self.image.size.height - 1) |
293 | + ctx.attributes.style = UNDERLINE |
294 | + ctx.print("S") |
295 | + ctx.move_to(28, self.image.size.height - 1) |
296 | + ctx.attributes.style = NORMAL |
297 | + ctx.print("elect All") |
298 | + ctx.move_to(41, self.image.size.height - 1) |
299 | + ctx.attributes.style = UNDERLINE |
300 | + ctx.print("D") |
301 | + ctx.move_to(42, self.image.size.height - 1) |
302 | + ctx.attributes.style = NORMAL |
303 | + ctx.print("eselect All") |
304 | + ctx.move_to(66 + extra_cols, self.image.size.height - 1) |
305 | + ctx.print("Start ") |
306 | + ctx.move_to(72 + extra_cols, self.image.size.height - 1) |
307 | + ctx.attributes.style = UNDERLINE |
308 | + ctx.print("T") |
309 | + ctx.move_to(73 + extra_cols, self.image.size.height - 1) |
310 | + ctx.attributes.style = NORMAL |
311 | + ctx.print("esting") |
312 | + for i, line in enumerate(self.tree.render(cols - 3)[self.top:bottom]): |
313 | + ctx.move_to(2, i + 2) |
314 | + if i != self.highlight: |
315 | + ctx.attributes.style = NORMAL |
316 | + else: # highlight the current line |
317 | + ctx.attributes.style = REVERSE |
318 | + ctx.print(line) |
319 | + |
320 | + def _selectNode(self): |
321 | + """ |
322 | + Mark a node/job as selected for this test run. |
323 | + See :meth:`SelectableJobTreeNode.set_ancestors_state()` and |
324 | + :meth:`SelectableJobTreeNode.set_descendants_state()` for details |
325 | + about the automatic selection of parents and descendants. |
326 | + """ |
327 | + node, category = self.tree.get_node_by_index(self.top + self.highlight) |
328 | + if category: # then the selected node is a job not a category |
329 | + job = node |
330 | + category.job_selection[job] = not(category.job_selection[job]) |
331 | + category.update_selected_state() |
332 | + category.set_ancestors_state(category.job_selection[job]) |
333 | + else: |
334 | + node.selected = not(node.selected) |
335 | + node.set_descendants_state(node.selected) |
336 | + node.set_ancestors_state(node.selected) |
337 | + |
338 | + def _toggleNode(self): |
339 | + """ |
340 | + Expand/collapse a node |
341 | + """ |
342 | + node, is_job = self.tree.get_node_by_index(self.top + self.highlight) |
343 | + if not is_job: |
344 | + node.expanded = not(node.expanded) |
345 | + |
346 | + def _scroll(self, direction): |
347 | + visible_length = len(self.tree.render()) |
348 | + # Scroll the tree view |
349 | + if (direction == "up" and |
350 | + self.highlight == 0 and self.top != 0): |
351 | + self.top -= 1 |
352 | + return |
353 | + elif (direction == "down" and |
354 | + (self.highlight + 1) == (self.image.size.height - 4) and |
355 | + (self.top + self.image.size.height - 4) != visible_length): |
356 | + self.top += 1 |
357 | + return |
358 | + # Move the highlighted line |
359 | + if (direction == "up" and |
360 | + (self.top != 0 or self.highlight != 0)): |
361 | + self.highlight -= 1 |
362 | + elif (direction == "down" and |
363 | + (self.top + self.highlight + 1) != visible_length and |
364 | + (self.highlight + 1) != (self.image.size.height - 4)): |
365 | + self.highlight += 1 |
366 | |
367 | |
368 | class CliInvocation(CheckBoxInvocationMixIn): |
369 | |
370 | - def __init__(self, provider_list, config, settings, ns): |
371 | + def __init__(self, provider_list, config, settings, ns, display=None): |
372 | super().__init__(provider_list) |
373 | self.provider_list = provider_list |
374 | self.config = config |
375 | self.settings = settings |
376 | + self.display = display |
377 | self.ns = ns |
378 | self.whitelists = [] |
379 | + self._local_only = False # Only run local jobs |
380 | if self.ns.whitelist: |
381 | for whitelist in self.ns.whitelist: |
382 | self.whitelists.append(WhiteList.from_file(whitelist.name)) |
383 | @@ -315,37 +457,6 @@ |
384 | elif self.ns.include_pattern_list: |
385 | self.whitelists.append(WhiteList(self.ns.include_pattern_list)) |
386 | |
387 | - if self.is_interactive: |
388 | - if self.settings['welcome_text']: |
389 | - try: |
390 | - curses.wrapper(show_welcome, self.settings['welcome_text']) |
391 | - except curses.error: |
392 | - raise SystemExit('Terminal size must be at least 80x24') |
393 | - if not self.whitelists: |
394 | - whitelists = [] |
395 | - for p in self.provider_list: |
396 | - if p.name in self.settings['default_providers']: |
397 | - whitelists.extend( |
398 | - [w.name for w in p.get_builtin_whitelists()]) |
399 | - try: |
400 | - selection = curses.wrapper(show_menu, "Suite selection", |
401 | - whitelists) |
402 | - except curses.error: |
403 | - raise SystemExit('Terminal size must be at least 80x24') |
404 | - if not selection: |
405 | - raise SystemExit('No whitelists selected, aborting...') |
406 | - for s in selection: |
407 | - self.whitelists.append( |
408 | - get_whitelist_by_name(provider_list, whitelists[s])) |
409 | - else: |
410 | - self.whitelists.append( |
411 | - get_whitelist_by_name( |
412 | - provider_list, self.settings['default_whitelist'])) |
413 | - |
414 | - print("[ Analyzing Jobs ]".center(80, '=')) |
415 | - self.session = None |
416 | - self.runner = None |
417 | - |
418 | @property |
419 | def is_interactive(self): |
420 | """ |
421 | @@ -358,7 +469,127 @@ |
422 | def run(self): |
423 | ns = self.ns |
424 | job_list = self.get_job_list(ns) |
425 | - return self._run_jobs(ns, job_list) |
426 | + previous_session_file = SessionStorageRepository().get_last_storage() |
427 | + resume_in_progress = False |
428 | + if previous_session_file: |
429 | + if self.is_interactive: |
430 | + if self.ask_for_resume(): |
431 | + resume_in_progress = True |
432 | + manager = SessionManager.load_session( |
433 | + job_list, previous_session_file) |
434 | + self._maybe_skip_last_job_after_resume(manager) |
435 | + else: |
436 | + resume_in_progress = True |
437 | + manager = SessionManager.load_session( |
438 | + job_list, previous_session_file) |
439 | + if not resume_in_progress: |
440 | + # Create a session that handles most of the stuff needed to run |
441 | + # jobs |
442 | + try: |
443 | + manager = SessionManager.create_session(job_list, |
444 | + legacy_mode=True) |
445 | + except DependencyDuplicateError as exc: |
446 | + # Handle possible DependencyDuplicateError that can happen if |
447 | + # someone is using plainbox for job development. |
448 | + print("The job database you are currently using is broken") |
449 | + print("At least two jobs contend for the name {0}".format( |
450 | + exc.job.name)) |
451 | + print("First job defined in: {0}".format(exc.job.origin)) |
452 | + print("Second job defined in: {0}".format( |
453 | + exc.duplicate_job.origin)) |
454 | + raise SystemExit(exc) |
455 | + manager.state.metadata.title = " ".join(sys.argv) |
456 | + if self.is_interactive: |
457 | + if self.display is None: |
458 | + self.display = get_display() |
459 | + if self.settings['welcome_text']: |
460 | + self.display.run( |
461 | + ShowWelcome(self.settings['welcome_text'])) |
462 | + if not self.whitelists: |
463 | + whitelists = [] |
464 | + for p in self.provider_list: |
465 | + if p.name in self.settings['default_providers']: |
466 | + whitelists.extend( |
467 | + [w.name for w in p.get_builtin_whitelists()]) |
468 | + selection = self.display.run(ShowMenu("Suite selection", |
469 | + whitelists)) |
470 | + if not selection: |
471 | + raise SystemExit('No whitelists selected, aborting...') |
472 | + for s in selection: |
473 | + self.whitelists.append( |
474 | + get_whitelist_by_name(self.provider_list, |
475 | + whitelists[s])) |
476 | + else: |
477 | + self.whitelists.append( |
478 | + get_whitelist_by_name( |
479 | + self.provider_list, |
480 | + self.settings['default_whitelist'])) |
481 | + manager.checkpoint() |
482 | + |
483 | + if self.is_interactive and not resume_in_progress: |
484 | + # Pre-run all local jobs |
485 | + desired_job_list = select_jobs( |
486 | + manager.state.job_list, |
487 | + [CompositeQualifier( |
488 | + self.whitelists + |
489 | + [NonLocalJobQualifier(inclusive=False)] |
490 | + )]) |
491 | + self._update_desired_job_list(manager, desired_job_list) |
492 | + # Ask the password before anything else in order to run local jobs |
493 | + # requiring privileges |
494 | + if self._auth_warmup_needed(manager): |
495 | + print("[ Authentication ]".center(80, '=')) |
496 | + return_code = authenticate_warmup() |
497 | + if return_code: |
498 | + raise SystemExit(return_code) |
499 | + self._local_only = True |
500 | + self._run_jobs(ns, manager) |
501 | + self._local_only = False |
502 | + |
503 | + if not resume_in_progress: |
504 | + # Run the rest of the desired jobs |
505 | + desired_job_list = select_jobs(manager.state.job_list, |
506 | + self.whitelists) |
507 | + self._update_desired_job_list(manager, desired_job_list) |
508 | + if self.is_interactive: |
509 | + # Ask the password before anything else in order to run jobs |
510 | + # requiring privileges |
511 | + if self._auth_warmup_needed(manager): |
512 | + print("[ Authentication ]".center(80, '=')) |
513 | + return_code = authenticate_warmup() |
514 | + if return_code: |
515 | + raise SystemExit(return_code) |
516 | + tree = SelectableJobTreeNode.create_tree( |
517 | + manager.state.run_list, |
518 | + legacy_mode=True) |
519 | + title = 'Choose tests to run on your system:' |
520 | + if self.display is None: |
521 | + self.display = get_display() |
522 | + self.display.run(ScrollableTreeNode(tree, title)) |
523 | + self._update_desired_job_list(manager, tree.selection) |
524 | + estimated_duration_auto, estimated_duration_manual = \ |
525 | + manager.state.get_estimated_duration() |
526 | + if estimated_duration_auto: |
527 | + print( |
528 | + "Estimated duration is {:.2f} " |
529 | + "for automated jobs.".format(estimated_duration_auto)) |
530 | + else: |
531 | + print( |
532 | + "Estimated duration cannot be " |
533 | + "determined for automated jobs.") |
534 | + if estimated_duration_manual: |
535 | + print( |
536 | + "Estimated duration is {:.2f} " |
537 | + "for manual jobs.".format(estimated_duration_manual)) |
538 | + else: |
539 | + print( |
540 | + "Estimated duration cannot be " |
541 | + "determined for manual jobs.") |
542 | + self._run_jobs(ns, manager) |
543 | + manager.destroy() |
544 | + |
545 | + # FIXME: sensible return value |
546 | + return 0 |
547 | |
548 | def ask_for_resume(self): |
549 | return self.ask_user( |
550 | @@ -375,8 +606,8 @@ |
551 | answer = input("{} [{}] ".format(prompt, ", ".join(allowed))) |
552 | return answer |
553 | |
554 | - def _maybe_skip_last_job_after_resume(self, session): |
555 | - last_job = session.metadata.running_job_name |
556 | + def _maybe_skip_last_job_after_resume(self, manager): |
557 | + last_job = manager.state.metadata.running_job_name |
558 | if last_job is None: |
559 | return |
560 | print("We have previously tried to execute {}".format(last_job)) |
561 | @@ -394,78 +625,42 @@ |
562 | elif action == 'run': |
563 | result = None |
564 | if result: |
565 | - session.update_job_result( |
566 | - session.job_state_map[last_job].job, result) |
567 | - session.metadata.running_job_name = None |
568 | - session.persistent_save() |
569 | - |
570 | - def _run_jobs(self, ns, job_list): |
571 | - # Create a session that handles most of the stuff needed to run jobs |
572 | - try: |
573 | - session = SessionState(job_list) |
574 | - except DependencyDuplicateError as exc: |
575 | - # Handle possible DependencyDuplicateError that can happen if |
576 | - # someone is using plainbox for job development. |
577 | - print("The job database you are currently using is broken") |
578 | - print("At least two jobs contend for the name {0}".format( |
579 | - exc.job.name)) |
580 | - print("First job defined in: {0}".format(exc.job.origin)) |
581 | - print("Second job defined in: {0}".format( |
582 | - exc.duplicate_job.origin)) |
583 | - raise SystemExit(exc) |
584 | - with session.open(): |
585 | - desired_job_list = [] |
586 | - for whitelist in self.whitelists: |
587 | - desired_job_list.extend(get_matching_job_list(job_list, |
588 | - whitelist)) |
589 | - self._update_desired_job_list(session, desired_job_list) |
590 | - if session.previous_session_file(): |
591 | - if self.is_interactive and self.ask_for_resume(): |
592 | - session.resume() |
593 | - self._maybe_skip_last_job_after_resume(session) |
594 | - else: |
595 | - session.clean() |
596 | - session.metadata.title = " ".join(sys.argv) |
597 | - session.persistent_save() |
598 | - # Ask the password before anything else in order to run jobs |
599 | - # requiring privileges |
600 | - if self.is_interactive and self._auth_warmup_needed(session): |
601 | - print("[ Authentication ]".center(80, '=')) |
602 | - return_code = authenticate_warmup() |
603 | - if return_code: |
604 | - raise SystemExit(return_code) |
605 | - runner = JobRunner( |
606 | - session.session_dir, self.provider_list, |
607 | - session.jobs_io_log_dir) |
608 | - self._run_jobs_with_session(ns, session, runner) |
609 | - self.save_results(session) |
610 | - session.remove() |
611 | - |
612 | - # FIXME: sensible return value |
613 | - return 0 |
614 | - |
615 | - def _auth_warmup_needed(self, session): |
616 | + manager.state.update_job_result( |
617 | + manager.state.job_state_map[last_job].job, result) |
618 | + manager.state.metadata.running_job_name = None |
619 | + manager.checkpoint() |
620 | + |
621 | + def _run_jobs(self, ns, manager): |
622 | + runner = JobRunner( |
623 | + manager.storage.location, self.provider_list, |
624 | + os.path.join(manager.storage.location, 'io-logs'), |
625 | + command_io_delegate=self) |
626 | + self._run_jobs_with_session(ns, manager, runner) |
627 | + if not self._local_only: |
628 | + self.save_results(manager) |
629 | + |
630 | + def _auth_warmup_needed(self, manager): |
631 | # Don't warm up plainbox-trusted-launcher-1 if none of the providers |
632 | # use it. We assume that the mere presence of a provider makes it |
633 | # possible for a root job to be preset but it could be improved to |
634 | - # acutally know when this step is absolutely not required (no local |
635 | + # actually know when this step is absolutely not required (no local |
636 | # jobs, no jobs |
637 | # need root) |
638 | if all(not provider.secure for provider in self.provider_list): |
639 | return False |
640 | # Don't use authentication warm-up if none of the jobs on the run list |
641 | # requires it. |
642 | - if all(job.user is None for job in session.run_list): |
643 | + if all(job.user is None for job in manager.state.run_list): |
644 | return False |
645 | # Otherwise, do pre-authentication |
646 | return True |
647 | |
648 | - def save_results(self, session): |
649 | + def save_results(self, manager): |
650 | if self.is_interactive: |
651 | print("[ Results ]".center(80, '=')) |
652 | exporter = get_all_exporters()['text']() |
653 | exported_stream = io.BytesIO() |
654 | - data_subset = exporter.get_session_data_subset(session) |
655 | + data_subset = exporter.get_session_data_subset(manager.state) |
656 | exporter.dump(data_subset, exported_stream) |
657 | exported_stream.seek(0) # Need to rewind the file, puagh |
658 | # This requires a bit more finesse, as exporters output bytes |
659 | @@ -490,7 +685,7 @@ |
660 | exporter = exporter_cls( |
661 | ['with-sys-info', 'with-summary', 'with-job-description', |
662 | 'with-text-attachments']) |
663 | - data_subset = exporter.get_session_data_subset(session) |
664 | + data_subset = exporter.get_session_data_subset(manager.state) |
665 | results_path = results_file |
666 | if exporter_cls is XMLSessionStateExporter: |
667 | results_path = submission_file |
668 | @@ -533,64 +728,54 @@ |
669 | result['comments'] = input('Please enter your comments:\n') |
670 | return DiskJobResult(result) |
671 | |
672 | - def _update_desired_job_list(self, session, desired_job_list): |
673 | - problem_list = session.update_desired_job_list(desired_job_list) |
674 | + def _update_desired_job_list(self, manager, desired_job_list): |
675 | + problem_list = manager.state.update_desired_job_list(desired_job_list) |
676 | if problem_list: |
677 | print("[ Warning ]".center(80, '*')) |
678 | print("There were some problems with the selected jobs") |
679 | for problem in problem_list: |
680 | print(" * {}".format(problem)) |
681 | print("Problematic jobs will not be considered") |
682 | - (estimated_duration_auto, |
683 | - estimated_duration_manual) = session.get_estimated_duration() |
684 | - if estimated_duration_auto: |
685 | - print("Estimated duration is {:.2f} for automated jobs.".format( |
686 | - estimated_duration_auto)) |
687 | - else: |
688 | - print( |
689 | - "Estimated duration cannot be determined for automated jobs.") |
690 | - if estimated_duration_manual: |
691 | - print("Estimated duration is {:.2f} for manual jobs.".format( |
692 | - estimated_duration_manual)) |
693 | - else: |
694 | - print("Estimated duration cannot be determined for manual jobs.") |
695 | |
696 | - def _run_jobs_with_session(self, ns, session, runner): |
697 | + def _run_jobs_with_session(self, ns, manager, runner): |
698 | # TODO: run all resource jobs concurrently with multiprocessing |
699 | # TODO: make local job discovery nicer, it would be best if |
700 | # desired_jobs could be managed entirely internally by SesionState. In |
701 | # such case the list of jobs to run would be changed during iteration |
702 | # but would be otherwise okay). |
703 | - print("[ Running All Jobs ]".center(80, '=')) |
704 | + if self._local_only: |
705 | + print("[ Loading Jobs Definition ]".center(80, '=')) |
706 | + else: |
707 | + print("[ Running All Jobs ]".center(80, '=')) |
708 | again = True |
709 | while again: |
710 | again = False |
711 | - for job in session.run_list: |
712 | + for job in manager.state.run_list: |
713 | # Skip jobs that already have result, this is only needed when |
714 | # we run over the list of jobs again, after discovering new |
715 | # jobs via the local job output |
716 | - if session.job_state_map[job.name].result.outcome is not None: |
717 | + if (manager.state.job_state_map[job.name].result.outcome |
718 | + is not None): |
719 | continue |
720 | - self._run_single_job_with_session(ns, session, runner, job) |
721 | - session.persistent_save() |
722 | + self._run_single_job_with_session(ns, manager, runner, job) |
723 | + manager.checkpoint() |
724 | if job.plugin == "local": |
725 | # After each local job runs rebuild the list of matching |
726 | # jobs and run everything again |
727 | - desired_job_list = [] |
728 | - for whitelist in self.whitelists: |
729 | - desired_job_list.extend( |
730 | - get_matching_job_list(session.job_list, whitelist)) |
731 | - self._update_desired_job_list(session, desired_job_list) |
732 | + desired_job_list = select_jobs(manager.state.job_list, |
733 | + self.whitelists) |
734 | + if self._local_only: |
735 | + desired_job_list = [ |
736 | + job for job in desired_job_list |
737 | + if job.plugin == 'local'] |
738 | + self._update_desired_job_list(manager, desired_job_list) |
739 | again = True |
740 | break |
741 | |
742 | - def _run_single_job_with_session(self, ns, session, runner, job): |
743 | - print("[ {} ]".format(job.name).center(80, '-')) |
744 | - if job.description is not None: |
745 | - print(job.description) |
746 | - print("^" * len(job.description.splitlines()[-1])) |
747 | - print() |
748 | - job_state = session.job_state_map[job.name] |
749 | + def _run_single_job_with_session(self, ns, manager, runner, job): |
750 | + if job.plugin not in ['local', 'resource']: |
751 | + print("[ {} ]".format(job.name).center(80, '-')) |
752 | + job_state = manager.state.job_state_map[job.name] |
753 | logger.debug("Job name: %s", job.name) |
754 | logger.debug("Plugin: %s", job.plugin) |
755 | logger.debug("Direct dependencies: %s", job.get_direct_dependencies()) |
756 | @@ -601,10 +786,15 @@ |
757 | logger.debug("Can start: %s", job_state.can_start()) |
758 | logger.debug("Readiness: %s", job_state.get_readiness_description()) |
759 | if job_state.can_start(): |
760 | - print("Running... (output in {}.*)".format( |
761 | - join(session.jobs_io_log_dir, slugify(job.name)))) |
762 | - session.metadata.running_job_name = job.name |
763 | - session.persistent_save() |
764 | + if job.plugin not in ['local', 'resource']: |
765 | + if job.description is not None: |
766 | + print(job.description) |
767 | + print("^" * len(job.description.splitlines()[-1])) |
768 | + print() |
769 | + print("Running... (output in {}.*)".format( |
770 | + join(manager.storage.location, slugify(job.name)))) |
771 | + manager.state.metadata.running_job_name = job.name |
772 | + manager.checkpoint() |
773 | # TODO: get a confirmation from the user for certain types of |
774 | # job.plugin |
775 | job_result = runner.run_job(job) |
776 | @@ -612,17 +802,21 @@ |
777 | and self.is_interactive): |
778 | job_result = self._interaction_callback( |
779 | runner, job, self.config) |
780 | - session.metadata.running_job_name = None |
781 | - session.persistent_save() |
782 | - print("Outcome: {}".format(job_result.outcome)) |
783 | - print("Comments: {}".format(job_result.comments)) |
784 | + manager.state.metadata.running_job_name = None |
785 | + manager.checkpoint() |
786 | + if job.plugin not in ['local', 'resource']: |
787 | + print("Outcome: {}".format(job_result.outcome)) |
788 | + if job_result.comments is not None: |
789 | + print("Comments: {}".format(job_result.comments)) |
790 | else: |
791 | job_result = MemoryJobResult({ |
792 | 'outcome': IJobResult.OUTCOME_NOT_SUPPORTED, |
793 | 'comments': job_state.get_readiness_description() |
794 | }) |
795 | + if job.plugin not in ['local', 'resource']: |
796 | + print("Outcome: {}".format(job_result.outcome)) |
797 | if job_result is not None: |
798 | - session.update_job_result(job, job_result) |
799 | + manager.state.update_job_result(job, job_result) |
800 | |
801 | |
802 | class CliCommand(PlainBoxCommand, CheckBoxCommandMixIn): |
This looks mostly okay. As said on IRC the only thing I found so far was the fact that you use SessionManager objects as 'session' where we previously had SessionState+ LegacyAPIs being the session. I would much rather see 'manager' instead.