Merge lp:~sylvain-pineau/checkbox/fixes into lp:checkbox

Proposed by Sylvain Pineau
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
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Zygmunt Krynicki (community) Approve
Review via email: mp+208082@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

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.

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Done, manager replaced the confusing word session;

review: Needs Resubmitting
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

Thanks, +1

review: Approve
Revision history for this message
Daniel Manrique (roadmr) wrote :
Download full text (3.7 KiB)

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+184077minor)pagefaults 0swaps
[precise] Starting tests...
[precise] Checkbox GUI build: PASS
[precise] (timing) 0.86user 0.30system 1:03.08elapsed 1%CPU (0avgtext+0avgdata 19896maxresident)k
[precise] (timing) 0inputs+64outputs (0major+61518minor)pagefaults 0swaps
[precise] CheckBox test suite: PASS
[precise] (timing) 0.84user 0.33system 0:38.11elapsed 3%CPU (0avgtext+0avgdata 20080maxresident)k
[precise] (timing) 0inputs+40outputs (0major+61843minor)pagefaults 0swaps
[precise] (timing) 0.80user 0.39system 0:05.99elapsed 19%CPU (0avgtext+0avgdata 20560maxresident)k
[precise] (timing) 0inputs+16outputs (0major+61166minor)pagefaults 0swaps
[precise] PlainBox test suite: PASS
[precise] (timing) 1.19user 0.35system 0:18.69elapsed 8%CPU (0avgtext+0avgdata 20188maxresident)k
[precise] (timing) 0inputs+344outputs (0major+61684minor)pagefaults 0swaps
[precise] PlainBox documentation build: PASS
[precise] (timing) 0.93user 0.37system 0:32.60elapsed 4%CPU (0avgtext+0avgdata 20780maxresident)k
[precise] (timing) 0inputs+32outputs (0major+62104minor)pagefaults 0swaps
[precise] CheckBoxNG test suite: FAIL
[precise] stdout: http://paste.ubuntu.com/6994285/
[precise] stderr: http://paste.ubuntu.com/6994286/
[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+61530minor)pagefaults 0swaps
[precise] Integration tests: PASS
[precise] (timing) 0.79user 0.34system 0:09.49elapsed 11%CPU (0avgtext+0avgdata 19992maxresident)k
[precise] (timing) 0inputs+8outputs (0major+61897minor)pagefaults 0swaps
[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+280263minor)pagefaults 0swaps
[trusty] Starting tests...
[trusty] Checkbox GUI build: PASS
[trusty] (timing) 1.22user 0.44system 1:08.98elapsed 2%CPU (0avgtext+0avgdata 20800maxresident)k
[trusty] (timing) 5944inputs+64outputs (75major+61184minor)pagefaults 0swaps
[trusty] CheckBox test suite: PASS
[trusty] (timing) 0.94user 0.36system 0:42.36elapsed 3%CPU (0avgtext+0avgdata 20652maxresident)k
[trusty] (timing) 0inputs+48outputs (0major+59794minor)pagefaults 0swaps
[trusty] (timing) 0.86user 0.30system 0:07.87elapsed 14%CPU (0avgtext+0avgdata 20636maxresident)k
[trusty] (timing) 0inputs+16outputs (0major+59521minor)pagefaults 0swaps
[trusty] PlainBox test suite: PASS
[trusty] (timing) 1.15user 0.40system 0:21.03elapsed 7%CPU (0avgtext+0avgdata 20504maxresident)k
[trusty] (timing) 0inputs+336outputs (0major+59617minor)pagefaults 0swaps
[trusty] PlainBox documentation build: PASS
[trusty] (timing) 0.84user 0.35sy...

Read more...

lp:~sylvain-pineau/checkbox/fixes updated
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 SelectableJobTreeNode.

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

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Re-approving, the textland import paths have been fixed.

review: Approve
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

I depend on https://code.launchpad.net/~sylvain-pineau/checkbox/textland_project_with_bzr_mv/+merge/208140
just waiting for it to land and I'll re-approve the MR.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):

Subscribers

People subscribed via source and target branches