Merge lp:~sylvain-pineau/checkbox/checkbox-ng-sa into lp:checkbox

Proposed by Sylvain Pineau
Status: Merged
Approved by: Maciej Kisielewski
Approved revision: 4125
Merged at revision: 4122
Proposed branch: lp:~sylvain-pineau/checkbox/checkbox-ng-sa
Merge into: lp:checkbox
Diff against target: 748 lines (+637/-5)
6 files modified
checkbox-ng/checkbox_ng/commands/sru.py (+1/-1)
checkbox-ng/checkbox_ng/misc.py (+22/-0)
checkbox-ng/checkbox_ng/ui.py (+4/-1)
checkbox-ng/launchers/checkbox-cli2 (+596/-0)
plainbox/plainbox/impl/session/assistant.py (+11/-3)
plainbox/plainbox/impl/session/restart.py (+3/-0)
To merge this branch: bzr merge lp:~sylvain-pineau/checkbox/checkbox-ng-sa
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Needs Resubmitting
Maciej Kisielewski Needs Fixing
Review via email: mp+275925@code.launchpad.net

Description of the change

checkbox-cli rewritten using plainbox SessionAssistant as a standalone tool (called checkbox-cli2 to avoid conflicts)

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

Hey. Thanks for sending this. I made several small comments below. I like the direction this is going but I'd like to overall streamline the code. I realize we cherry picked functions from various locations but now we have a chance to simplify and unify this so that it is easier to understand.

I would also like to know exactly which features we aim to support here (in other words, this is not a launcher interpreter). It would be good if we could write this down in the docstring of the module.

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

Ready for review, I put the app in the launcher folder to keep it in checkbox-ng for packaging needs

review: Needs Resubmitting
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

My only -1 is on the `._export_results`. I really believe that talking to plainbox internals behind SessionAssistant could be avoided. (See inline comment).

Building tree (create_simple_tree) smells bad, the nested coprehension in the loop.
But after understanding how the code works I guess that's the cleanest way (reading-wise) of doing it, and the list of categories in the root node *should* be small.

Super minor thing: rev 4121 ‘aa’ typo in commit msg

review: Needs Fixing
4121. By Sylvain Pineau

plainbox:session:assistant: Reset running_job_name when resuming with autorestart

When a session is resumed in autorestart mode, the running_job_name meta
data has to be reset and local self._metadata returned instead of the initial
self._resume_candidates[session_id].metadata.

4122. By Sylvain Pineau

plainbox:session:assistant: Create a checkpoint when calling use_job_result()

4123. By Sylvain Pineau

plainbox:session:restart: Append $SHELL to xdg desktop file command

To avoid closing the terminal when the resume command is over.

4124. By Sylvain Pineau

plainbox:session:assistant: Set FLAG_SUBMITTED after calling transport.send()

SessionAssistant export_to_transport() should set the flag FLAG_SUBMITTED if
the call to transport.send() succeeded.

4125. By Sylvain Pineau

checkbox-ng:checkbox-cli2: New CLI application based on Session Assistant

Not a true launcher but a standalone application aimed to replace checkbox-cli.
It relies on plainbox new Session Assistant to perform all low level operations.

The main differences between this version and current checkbox-cli are:
 - support for restart API
 - jobs are grouped by categories units (as opposed to Job.via links) in test
   selection screens.

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

_export_results fixed to use sa export to stream. and rev 4121 typo

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

Time to fix ppa packaging now

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'checkbox-ng/checkbox_ng/commands/sru.py'
--- checkbox-ng/checkbox_ng/commands/sru.py 2015-09-07 13:40:15 +0000
+++ checkbox-ng/checkbox_ng/commands/sru.py 2015-12-02 09:12:28 +0000
@@ -128,7 +128,7 @@
128128
129 def _collect_info(self, rc, sa):129 def _collect_info(self, rc, sa):
130 sa.select_providers('*')130 sa.select_providers('*')
131 sa.start_new_session(_("Hardware Collection Session"))131 sa.start_new_session(_("SRU Session"))
132 sa.select_test_plan(self.config.test_plan)132 sa.select_test_plan(self.config.test_plan)
133 sa.bootstrap()133 sa.bootstrap()
134 for job_id in sa.get_static_todo_list():134 for job_id in sa.get_static_todo_list():
135135
=== modified file 'checkbox-ng/checkbox_ng/misc.py'
--- checkbox-ng/checkbox_ng/misc.py 2015-10-13 13:41:17 +0000
+++ checkbox-ng/checkbox_ng/misc.py 2015-12-02 09:12:28 +0000
@@ -145,6 +145,28 @@
145 builder.auto_add_job(job)145 builder.auto_add_job(job)
146 return builder.root_node146 return builder.root_node
147147
148 @classmethod
149 def create_simple_tree(cls, sa, job_list):
150 """
151 Build a rooted JobTreeNode from a job list.
152
153 :argument sa:
154 A session assistant object
155 :argument job_list:
156 List of jobs to consider for building the tree.
157 """
158 root_node = cls()
159 for job in job_list:
160 cat_name = sa.get_category(job.category_id).tr_name()
161 matches = [n for n in root_node.categories if n.name == cat_name]
162 if not matches:
163 node = cls(cat_name)
164 root_node.add_category(node)
165 else:
166 node = matches[0]
167 node.add_job(job)
168 return root_node
169
148170
149class TreeBuilder:171class TreeBuilder:
150172
151173
=== modified file 'checkbox-ng/checkbox_ng/ui.py'
--- checkbox-ng/checkbox_ng/ui.py 2015-06-10 15:04:50 +0000
+++ checkbox-ng/checkbox_ng/ui.py 2015-12-02 09:12:28 +0000
@@ -77,12 +77,13 @@
77 """77 """
78 Display the appropriate menu and return the selected options78 Display the appropriate menu and return the selected options
79 """79 """
80 def __init__(self, title, menu, selection=[0]):80 def __init__(self, title, menu, selection=[0], multiple_allowed=True):
81 self.image = TextImage(Size(0, 0))81 self.image = TextImage(Size(0, 0))
82 self.title = title82 self.title = title
83 self.menu = menu83 self.menu = menu
84 self.option_count = len(menu)84 self.option_count = len(menu)
85 self.position = 0 # Zero-based index of the selected menu option85 self.position = 0 # Zero-based index of the selected menu option
86 self.multiple_allowed = multiple_allowed
86 if self.option_count:87 if self.option_count:
87 self.selection = selection88 self.selection = selection
88 else:89 else:
@@ -110,6 +111,8 @@
110 self.selection.remove(self.position)111 self.selection.remove(self.position)
111 elif self.position < self.option_count:112 elif self.position < self.option_count:
112 self.selection.append(self.position)113 self.selection.append(self.position)
114 if not self.multiple_allowed:
115 self.selection = [self.position]
113 self.repaint(event)116 self.repaint(event)
114 return self.image117 return self.image
115118
116119
=== added file 'checkbox-ng/launchers/checkbox-cli2'
--- checkbox-ng/launchers/checkbox-cli2 1970-01-01 00:00:00 +0000
+++ checkbox-ng/launchers/checkbox-cli2 2015-12-02 09:12:28 +0000
@@ -0,0 +1,596 @@
1#!/usr/bin/env python3
2# This file is part of Checkbox.
3#
4# Copyright 2015 Canonical Ltd.
5# Written by:
6# Sylvain Pineau <sylvain.pineau@canonical.com>
7#
8# Checkbox is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 3,
10# as published by the Free Software Foundation.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21Checkbox CLI Application.
22
23WARNING: this is not a launcher interpreter.
24"""
25
26from argparse import SUPPRESS
27from shutil import copyfileobj
28import gettext
29import io
30import json
31import logging
32import os
33import sys
34
35from guacamole import Command
36from guacamole.core import Ingredient
37from guacamole.ingredients import ansi
38from guacamole.ingredients import argparse
39from guacamole.ingredients import cmdtree
40from guacamole.recipes.cmd import CommandRecipe
41
42# TODO: use public APIs here
43from plainbox.abc import IJobResult
44from plainbox.i18n import ngettext
45from plainbox.i18n import pgettext as C_
46from plainbox.impl.commands.inv_run import seconds_to_human_duration
47from plainbox.impl.commands.inv_run import Action
48from plainbox.impl.commands.inv_run import ActionUI
49from plainbox.impl.commands.inv_run import NormalUI
50from plainbox.impl.commands.inv_run import ReRunJob
51from plainbox.impl.color import Colorizer
52from plainbox.impl.exporter import ByteStringStreamTranslator
53from plainbox.impl.ingredients import CanonicalCrashIngredient
54from plainbox.impl.ingredients import RenderingContextIngredient
55from plainbox.impl.ingredients import SessionAssistantIngredient
56from plainbox.impl.result import tr_outcome
57from plainbox.impl.result import JobResultBuilder
58from plainbox.impl.result import MemoryJobResult
59from plainbox.impl.session.assistant import SA_RESTARTABLE
60from plainbox.impl.session.jobs import InhibitionCause
61from plainbox.vendor.textland import get_display
62
63from checkbox_ng.misc import SelectableJobTreeNode
64from checkbox_ng.ui import ScrollableTreeNode
65from checkbox_ng.ui import ShowMenu
66from checkbox_ng.ui import ShowRerun
67
68
69_ = gettext.gettext
70
71_logger = logging.getLogger("checkbox-cli")
72
73
74class DisplayIngredient(Ingredient):
75
76 """Ingredient that adds a Textland display to guacamole."""
77
78 def late_init(self, context):
79 """Add a DisplayIngredient as ``display`` to the guacamole context."""
80 context.display = get_display()
81
82
83class CheckboxCommandRecipe(CommandRecipe):
84
85 """A recipe for using Checkbox-enhanced commands."""
86
87 def get_ingredients(self):
88 """Get a list of ingredients for guacamole."""
89 return [
90 cmdtree.CommandTreeBuilder(self.command),
91 cmdtree.CommandTreeDispatcher(),
92 argparse.ParserIngredient(),
93 CanonicalCrashIngredient(),
94 ansi.ANSIIngredient(),
95 SessionAssistantIngredient(),
96 RenderingContextIngredient(),
97 DisplayIngredient()
98 ]
99
100
101class CheckboxUI(NormalUI):
102
103 def considering_job(self, job, job_state):
104 pass
105
106
107class CheckboxCommand(Command):
108
109 """
110 A command with Checkbox-enhanced ingredients.
111
112 This command has two additional items in the guacamole execution context,
113 the :class:`DisplayIngredient` object ``display`` and the
114 :class:`SessionAssistant` object ``sa``.
115 """
116
117 bug_report_url = "https://bugs.launchpad.net/checkbox-ng/+filebug"
118
119 def main(self, argv=None, exit=True):
120 """
121 Shortcut for running a command.
122
123 See :meth:`guacamole.recipes.Recipe.main()` for details.
124 """
125 return CheckboxCommandRecipe(self).main(argv, exit)
126
127
128class checkbox_cli(CheckboxCommand):
129
130 """Tool to run Checkbox jobs interactively from the command line."""
131
132 app_id = 'checkbox-cli'
133
134 def get_sa_api_version(self):
135 return '0.99'
136
137 def get_sa_api_flags(self):
138 return (SA_RESTARTABLE,)
139
140 def register_arguments(self, parser):
141 """Method called to register command line arguments."""
142 parser.add_argument(
143 '-t', '--test-plan', action="store", metavar=_("TEST-PLAN-ID"),
144 default=None,
145 # TRANSLATORS: this is in imperative form
146 help=_("load the specified test plan"))
147 parser.add_argument(
148 '--secure_id', metavar="SECURE_ID",
149 help=_("Canonical hardware identifier (optional)"))
150 parser.add_argument(
151 '--non-interactive', action='store_true',
152 help=_("skip tests that require interactivity"))
153 parser.add_argument(
154 '--dont-suppress-output', action="store_true", default=False,
155 help=_("don't suppress the output of certain job plugin types"))
156 parser.add_argument(
157 '--staging', action='store_true', default=False,
158 # Hide staging from help message (See pad.lv/1350005)
159 help=SUPPRESS)
160 parser.add_argument(
161 '--resume', dest='session_id', metavar="SESSION_ID",
162 help=SUPPRESS)
163
164 def invoked(self, ctx):
165 """Method called when the command is invoked."""
166 self.ctx = ctx
167 self.transport = self._create_transport()
168 self.C = Colorizer()
169 try:
170 self._do_normal_sequence()
171 self._export_results()
172 if ctx.args.secure_id:
173 self._send_results()
174 ctx.sa.finalize_session()
175 except KeyboardInterrupt:
176 return 1
177
178 def _export_results(self):
179 if self.is_interactive:
180 print(self.C.header(_("Results")))
181 # This requires a bit more finesse, as exporters output bytes
182 # and stdout needs a string.
183 translating_stream = ByteStringStreamTranslator(
184 sys.stdout, "utf-8")
185 self.ctx.sa.export_to_stream(
186 '2013.com.canonical.plainbox::text', (), translating_stream)
187 base_dir = os.path.join(
188 os.getenv(
189 'XDG_DATA_HOME', os.path.expanduser("~/.local/share/")),
190 "checkbox-ng")
191 if not os.path.exists(base_dir):
192 os.makedirs(base_dir)
193 exp_options = ['with-sys-info', 'with-summary', 'with-job-description',
194 'with-text-attachments', 'with-certification-status',
195 'with-job-defs', 'with-io-log', 'with-comments']
196 exporters = [
197 '2013.com.canonical.plainbox::hexr',
198 '2013.com.canonical.plainbox::html',
199 '2013.com.canonical.plainbox::xlsx',
200 '2013.com.canonical.plainbox::json',
201 ]
202 print()
203 for unit_name in exporters:
204 results_path = self.ctx.sa.export_to_file(unit_name, exp_options,
205 base_dir)
206 print("file://{}".format(results_path))
207
208 def _send_results(self):
209 print()
210 print(_("Sending hardware report to Canonical Certification"))
211 print(_("Server URL is: {0}").format(self.transport.url))
212 result = self.ctx.sa.export_to_transport(
213 "2013.com.canonical.plainbox::hexr", self.transport)
214 if 'url' in result:
215 print(result['url'])
216
217 def _create_transport(self):
218 if self.ctx.args.secure_id:
219 return self.ctx.sa.get_canonical_certification_transport(
220 self.ctx.args.secure_id, staging=self.ctx.args.staging)
221
222 def _get_interactively_picked_testplans(self):
223 test_plan_ids = self.ctx.sa.get_test_plans()
224 test_plan_names = [self.ctx.sa.get_test_plan(tp_id).name for tp_id in
225 test_plan_ids]
226 try:
227 selected_index = self.ctx.display.run(
228 ShowMenu(_("Select test plan"),
229 test_plan_names, [],
230 multiple_allowed=False))[0]
231 except IndexError:
232 return None
233 return test_plan_ids[selected_index]
234
235 def _interactively_pick_jobs_to_run(self):
236 job_list = [self.ctx.sa.get_job(job_id) for job_id in
237 self.ctx.sa.get_static_todo_list()]
238 tree = SelectableJobTreeNode.create_simple_tree(self.ctx.sa, job_list)
239 title = _('Choose tests to run on your system:')
240 self.ctx.display.run(ScrollableTreeNode(tree, title))
241 # NOTE: tree.selection is correct but ordered badly. To retain
242 # the original ordering we should just treat it as a mask and
243 # use it to filter jobs from get_static_todo_list.
244 wanted_set = frozenset([job.id for job in tree.selection])
245 job_id_list = [job_id for job_id in self.ctx.sa.get_static_todo_list()
246 if job_id in wanted_set]
247 self.ctx.sa.use_alternate_selection(job_id_list)
248
249 @property
250 def is_interactive(self):
251 """
252 Flag indicating that this is an interactive invocation.
253
254 We can then interact with the user when we encounter OUTCOME_UNDECIDED.
255 """
256 return (sys.stdin.isatty() and sys.stdout.isatty() and not
257 self.ctx.args.non_interactive)
258
259 def _get_ui_for_job(self, job):
260 if self.ctx.args.dont_suppress_output is False and job.plugin in (
261 'local', 'resource', 'attachment'):
262 return CheckboxUI(self.C.c, show_cmd_output=False)
263 else:
264 return CheckboxUI(self.C.c, show_cmd_output=True)
265
266 def _run_single_job_with_ui_loop(self, job, ui):
267 print(self.C.header(job.tr_summary(), fill='-'))
268 print(_("ID: {0}").format(job.id))
269 print(_("Category: {0}").format(
270 self.ctx.sa.get_job_state(job.id).effective_category_id))
271 comments = ""
272 while True:
273 if job.plugin in ('user-interact', 'user-interact-verify',
274 'user-verify', 'manual'):
275 ui.notify_about_purpose(job)
276 if (self.is_interactive and
277 job.plugin in ('user-interact',
278 'user-interact-verify',
279 'manual')):
280 ui.notify_about_steps(job)
281 if job.plugin == 'manual':
282 cmd = 'run'
283 else:
284 cmd = ui.wait_for_interaction_prompt(job)
285 if cmd == 'run' or cmd is None:
286 result_builder = self.ctx.sa.run_job(job.id, ui, False)
287 elif cmd == 'comment':
288 new_comment = input(self.C.BLUE(
289 _('Please enter your comments:') + '\n'))
290 if new_comment:
291 comments += new_comment + '\n'
292 continue
293 elif cmd == 'skip':
294 result_builder = JobResultBuilder(
295 outcome=IJobResult.OUTCOME_SKIP,
296 comments=_("Explicitly skipped before"
297 " execution"))
298 if comments != "":
299 result_builder.comments = comments
300 break
301 elif cmd == 'quit':
302 raise SystemExit()
303 else:
304 result_builder = self.ctx.sa.run_job(job.id, ui, False)
305 else:
306 if 'noreturn' in job.get_flag_set():
307 ui.noreturn_job()
308 result_builder = self.ctx.sa.run_job(job.id, ui, False)
309 if (self.is_interactive and
310 result_builder.outcome == IJobResult.OUTCOME_UNDECIDED):
311 try:
312 if comments != "":
313 result_builder.comments = comments
314 ui.notify_about_verification(job)
315 self._interaction_callback(job, result_builder)
316 except ReRunJob:
317 self.ctx.sa.use_job_result(job.id,
318 result_builder.get_result())
319 continue
320 break
321 return result_builder
322
323 def _pick_action_cmd(self, action_list, prompt=None):
324 return ActionUI(action_list, prompt).run()
325
326 def _interaction_callback(self, job, result_builder,
327 prompt=None, allowed_outcome=None):
328 result = result_builder.get_result()
329 if prompt is None:
330 prompt = _("Select an outcome or an action: ")
331 if allowed_outcome is None:
332 allowed_outcome = [IJobResult.OUTCOME_PASS,
333 IJobResult.OUTCOME_FAIL,
334 IJobResult.OUTCOME_SKIP]
335 allowed_actions = [
336 Action('c', _('add a comment'), 'set-comments')
337 ]
338 if IJobResult.OUTCOME_PASS in allowed_outcome:
339 allowed_actions.append(
340 Action('p', _('set outcome to {0}').format(
341 self.C.GREEN(C_('set outcome to <pass>', 'pass'))),
342 'set-pass'))
343 if IJobResult.OUTCOME_FAIL in allowed_outcome:
344 allowed_actions.append(
345 Action('f', _('set outcome to {0}').format(
346 self.C.RED(C_('set outcome to <fail>', 'fail'))),
347 'set-fail'))
348 if IJobResult.OUTCOME_SKIP in allowed_outcome:
349 allowed_actions.append(
350 Action('s', _('set outcome to {0}').format(
351 self.C.YELLOW(C_('set outcome to <skip>', 'skip'))),
352 'set-skip'))
353 if job.command is not None:
354 allowed_actions.append(
355 Action('r', _('re-run this job'), 're-run'))
356 if result.return_code is not None:
357 if result.return_code == 0:
358 suggested_outcome = IJobResult.OUTCOME_PASS
359 else:
360 suggested_outcome = IJobResult.OUTCOME_FAIL
361 allowed_actions.append(
362 Action('', _('set suggested outcome [{0}]').format(
363 tr_outcome(suggested_outcome)), 'set-suggested'))
364 while result.outcome not in allowed_outcome:
365 print(_("Please decide what to do next:"))
366 print(" " + _("outcome") + ": {0}".format(
367 self.C.result(result)))
368 if result.comments is None:
369 print(" " + _("comments") + ": {0}".format(
370 C_("none comment", "none")))
371 else:
372 print(" " + _("comments") + ": {0}".format(
373 self.C.CYAN(result.comments, bright=False)))
374 cmd = self._pick_action_cmd(allowed_actions)
375 if cmd == 'set-pass':
376 result_builder.outcome = IJobResult.OUTCOME_PASS
377 elif cmd == 'set-fail':
378 result_builder.outcome = IJobResult.OUTCOME_FAIL
379 elif cmd == 'set-skip' or cmd is None:
380 result_builder.outcome = IJobResult.OUTCOME_SKIP
381 elif cmd == 'set-suggested':
382 result_builder.outcome = suggested_outcome
383 elif cmd == 'set-comments':
384 new_comment = input(self.C.BLUE(
385 _('Please enter your comments:') + '\n'))
386 if new_comment:
387 result_builder.add_comment(new_comment)
388 elif cmd == 're-run':
389 raise ReRunJob
390 result = result_builder.get_result()
391
392 def _run_jobs(self, jobs_to_run):
393 estimated_time = 0
394 for job_id in jobs_to_run:
395 job = self.ctx.sa.get_job(job_id)
396 if (job.estimated_duration is not None
397 and estimated_time is not None):
398 estimated_time += job.estimated_duration
399 else:
400 estimated_time = None
401 for job_no, job_id in enumerate(jobs_to_run, start=1):
402 print(self.C.header(
403 _('Running job {} / {}. Estimated time left: {}').format(
404 job_no, len(jobs_to_run),
405 seconds_to_human_duration(max(0, estimated_time))
406 if estimated_time is not None else _("unknown")),
407 fill='-'))
408 job = self.ctx.sa.get_job(job_id)
409 builder = self._run_single_job_with_ui_loop(
410 job, self._get_ui_for_job(job))
411 result = builder.get_result()
412 self.ctx.sa.use_job_result(job_id, result)
413 if (job.estimated_duration is not None
414 and estimated_time is not None):
415 estimated_time -= job.estimated_duration
416
417 def _get_rerun_candidates(self):
418 """Get all the tests that might be selected for rerunning."""
419 def rerun_predicate(job_state):
420 return job_state.result.outcome in (
421 IJobResult.OUTCOME_FAIL, IJobResult.OUTCOME_CRASH,
422 IJobResult.OUTCOME_NOT_SUPPORTED, IJobResult.OUTCOME_SKIP)
423 rerun_candidates = []
424 todo_list = self.ctx.sa.get_static_todo_list()
425 job_states = {job_id: self.ctx.sa.get_job_state(job_id) for job_id
426 in todo_list}
427 for job_id, job_state in job_states.items():
428 if rerun_predicate(job_state):
429 rerun_candidates.append(self.ctx.sa.get_job(job_id))
430 return rerun_candidates
431
432 def _maybe_rerun_jobs(self):
433 # create a list of jobs that qualify for rerunning
434 rerun_candidates = self._get_rerun_candidates()
435 # bail-out early if no job qualifies for rerunning
436 if not rerun_candidates:
437 return False
438 tree = SelectableJobTreeNode.create_simple_tree(self.ctx.sa,
439 rerun_candidates)
440 # nothing to select in root node and categories - bailing out
441 if not tree.jobs and not tree._categories:
442 return False
443 # deselect all by default
444 tree.set_descendants_state(False)
445 self.ctx.display.run(ShowRerun(tree, _("Select jobs to re-run")))
446 wanted_set = frozenset(tree.selection)
447 if not wanted_set:
448 # nothing selected - nothing to run
449 return False
450 rerun_candidates = []
451 # include resource jobs that selected jobs depend on
452 resources_to_rerun = []
453 for job in wanted_set:
454 job_state = self.ctx.sa.get_job_state(job.id)
455 for inhibitor in job_state.readiness_inhibitor_list:
456 if inhibitor.cause == InhibitionCause.FAILED_DEP:
457 resources_to_rerun.append(inhibitor.related_job)
458 # reset outcome of jobs that are selected for re-running
459 for job in list(wanted_set) + resources_to_rerun:
460 self.ctx.sa.get_job_state(job.id).result = MemoryJobResult({})
461 rerun_candidates.append(job.id)
462 self._run_jobs(rerun_candidates)
463 return True
464
465 def _do_normal_sequence(self):
466 self.ctx.sa.select_providers("*")
467 self.ctx.sa.configure_application_restart(
468 lambda session_id: [
469 'sh', '-c', ' '.join([
470 os.path.abspath(__file__),
471 "--resume", session_id])
472 ])
473 resumed = self._maybe_resume_session()
474 if not resumed:
475 print(_("Preparing..."))
476 self.ctx.sa.start_new_session(_("Checkbox CLI Session"))
477 testplan_id = None
478 if self.ctx.args.test_plan:
479 if self.ctx.args.test_plan in self.ctx.sa.get_test_plans():
480 testplan_id = self.ctx.args.test_plan
481 elif self.is_interactive:
482 testplan_id = self._get_interactively_picked_testplans()
483 if not testplan_id:
484 self.ctx.rc.reset()
485 self.ctx.rc.bg = 'red'
486 self.ctx.rc.fg = 'bright_white'
487 self.ctx.rc.bold = 1
488 self.ctx.rc.para(_("Test plan not found!"))
489 raise SystemExit(1)
490 self.ctx.sa.select_test_plan(testplan_id)
491 self.ctx.sa.update_app_blob(json.dumps(
492 {'testplan_id': testplan_id, }).encode("UTF-8"))
493 self.ctx.sa.bootstrap()
494 if self.is_interactive:
495 self._interactively_pick_jobs_to_run()
496 self._run_jobs(self.ctx.sa.get_dynamic_todo_list())
497 if self.is_interactive:
498 while True:
499 if self._maybe_rerun_jobs():
500 continue
501 else:
502 break
503
504 def _handle_last_job_after_resume(self, metadata):
505 last_job = metadata.running_job_name
506 if last_job is None:
507 return
508 print(_("Previous session run tried to execute job: {}").format(
509 last_job))
510 cmd = self._pick_action_cmd([
511 Action('s', _("skip that job"), 'skip'),
512 Action('p', _("mark it as passed and continue"), 'pass'),
513 Action('f', _("mark it as failed and continue"), 'fail'),
514 Action('r', _("run it again"), 'run'),
515 ], _("What do you want to do with that job?"))
516 if cmd == 'skip' or cmd is None:
517 result = MemoryJobResult({
518 'outcome': IJobResult.OUTCOME_SKIP,
519 'comments': _("Skipped after resuming execution")
520 })
521 elif cmd == 'pass':
522 result = MemoryJobResult({
523 'outcome': IJobResult.OUTCOME_PASS,
524 'comments': _("Passed after resuming execution")
525 })
526 elif cmd == 'fail':
527 result = MemoryJobResult({
528 'outcome': IJobResult.OUTCOME_FAIL,
529 'comments': _("Failed after resuming execution")
530 })
531 elif cmd == 'run':
532 result = None
533 if result:
534 self.ctx.sa.use_job_result(last_job, result)
535
536 def _maybe_resume_session(self):
537 # Try to use the first session that can be resumed if the user agrees
538 resume_candidates = list(self.ctx.sa.get_resumable_sessions())
539 resumed = False
540 if resume_candidates:
541 if self.ctx.args.session_id:
542 for candidate in resume_candidates:
543 if candidate.id == self.args.session_id:
544 resume_candidates = (candidate, )
545 break
546 else:
547 raise RuntimeError("Requested session is not resumable!")
548 elif self.is_interactive:
549 print(self.C.header(_("Resume Incomplete Session")))
550 print(ngettext(
551 "There is {0} incomplete session that might be resumed",
552 "There are {0} incomplete sessions that might be resumed",
553 len(resume_candidates)
554 ).format(len(resume_candidates)))
555 else:
556 return
557 for candidate in resume_candidates:
558 if self.ctx.args.session_id:
559 cmd = 'resume'
560 else:
561 # Skip sessions that the user doesn't want to resume
562 cmd = self._pick_action_cmd([
563 Action('r', _("resume this session"), 'resume'),
564 Action('n', _("next session"), 'next'),
565 Action('c', _("create new session"), 'create')
566 ], _("Do you want to resume session {0!a}?").format(
567 candidate.id))
568 if cmd == 'next':
569 continue
570 elif cmd == 'create' or cmd is None:
571 break
572 elif cmd == 'resume':
573 metadata = self.ctx.sa.resume_session(candidate.id)
574 app_blob = json.loads(metadata.app_blob.decode("UTF-8"))
575 test_plan_id = app_blob['testplan_id']
576 # FIXME selecting again the testplan on resume resets both
577 # the static and dynamic todo lists.
578 # We're then saving the selection from the saved run_list
579 # by accessing the session private context object.
580 selected_id_list = [job.id for job in
581 self.ctx.sa._context.state.run_list]
582 self.ctx.sa.select_test_plan(test_plan_id)
583 self.ctx.sa.bootstrap()
584 self.ctx.sa.use_alternate_selection(selected_id_list)
585 # If we resumed maybe not rerun the same, probably broken
586 # job
587 self._handle_last_job_after_resume(metadata)
588 self._run_jobs(self.ctx.sa.get_dynamic_todo_list())
589 resumed = True
590 # Finally ignore other sessions that can be resumed
591 break
592 return resumed
593
594
595if __name__ == '__main__':
596 checkbox_cli().main()
0597
=== modified file 'plainbox/plainbox/impl/session/assistant.py'
--- plainbox/plainbox/impl/session/assistant.py 2015-11-04 14:25:10 +0000
+++ plainbox/plainbox/impl/session/assistant.py 2015-12-02 09:12:28 +0000
@@ -551,6 +551,8 @@
551 io_log_filename=self._runner.get_record_path_for_job(job),551 io_log_filename=self._runner.get_record_path_for_job(job),
552 ).get_result()552 ).get_result()
553 self._context.state.update_job_result(job, result)553 self._context.state.update_job_result(job, result)
554 self._metadata.running_job_name = None
555 self._manager.checkpoint()
554 if self._restart_strategy is not None:556 if self._restart_strategy is not None:
555 self._restart_strategy.diffuse_application_restart(self._app_id)557 self._restart_strategy.diffuse_application_restart(self._app_id)
556 self.session_available(self._manager.storage.id)558 self.session_available(self._manager.storage.id)
@@ -558,7 +560,7 @@
558 UsageExpectation.of(self).allowed_calls = {560 UsageExpectation.of(self).allowed_calls = {
559 self.select_test_plan: "to save test plan selection",561 self.select_test_plan: "to save test plan selection",
560 }562 }
561 return self._resume_candidates[session_id].metadata563 return self._metadata
562564
563 @raises(UnexpectedMethodCall)565 @raises(UnexpectedMethodCall)
564 def get_resumable_sessions(self) -> 'Tuple[str, SessionMetaData]':566 def get_resumable_sessions(self) -> 'Tuple[str, SessionMetaData]':
@@ -1187,6 +1189,7 @@
1187 UsageExpectation.of(self).enforce()1189 UsageExpectation.of(self).enforce()
1188 job = self._context.get_unit(job_id, 'job')1190 job = self._context.get_unit(job_id, 'job')
1189 self._context.state.update_job_result(job, result)1191 self._context.state.update_job_result(job, result)
1192 self._manager.checkpoint()
1190 # Set up expectations so that run_job() and use_job_result() must be1193 # Set up expectations so that run_job() and use_job_result() must be
1191 # called in pairs and applications cannot just forget and call1194 # called in pairs and applications cannot just forget and call
1192 # run_job() all the time.1195 # run_job() all the time.
@@ -1284,7 +1287,11 @@
1284 exported_stream = io.BytesIO()1287 exported_stream = io.BytesIO()
1285 exporter.dump_from_session_manager(self._manager, exported_stream)1288 exporter.dump_from_session_manager(self._manager, exported_stream)
1286 exported_stream.seek(0)1289 exported_stream.seek(0)
1287 return transport.send(exported_stream)1290 result = transport.send(exported_stream)
1291 if SessionMetaData.FLAG_SUBMITTED not in self._metadata.flags:
1292 self._metadata.flags.add(SessionMetaData.FLAG_SUBMITTED)
1293 self._manager.checkpoint()
1294 return result
12881295
1289 @raises(KeyError, OSError)1296 @raises(KeyError, OSError)
1290 def export_to_file(1297 def export_to_file(
@@ -1312,7 +1319,8 @@
1312 When there is a problem when writing the output.1319 When there is a problem when writing the output.
1313 """1320 """
1314 UsageExpectation.of(self).enforce()1321 UsageExpectation.of(self).enforce()
1315 exporter = self._manager.create_exporter(exporter_id, option_list)1322 exporter = self._manager.create_exporter(exporter_id, option_list,
1323 strict=False)
1316 timestamp = datetime.datetime.utcnow().isoformat()1324 timestamp = datetime.datetime.utcnow().isoformat()
1317 path = os.path.join(dir_path, ''.join(1325 path = os.path.join(dir_path, ''.join(
1318 ['submission_', timestamp, '.', exporter.unit.file_extension]))1326 ['submission_', timestamp, '.', exporter.unit.file_extension]))
13191327
=== modified file 'plainbox/plainbox/impl/session/restart.py'
--- plainbox/plainbox/impl/session/restart.py 2015-11-04 14:25:10 +0000
+++ plainbox/plainbox/impl/session/restart.py 2015-12-02 09:12:28 +0000
@@ -75,6 +75,7 @@
75 The command callback75 The command callback
76 """76 """
77 self.config = config = PlainBoxConfigParser()77 self.config = config = PlainBoxConfigParser()
78 self.app_terminal = app_terminal
78 section = 'Desktop Entry'79 section = 'Desktop Entry'
79 config.add_section(section)80 config.add_section(section)
80 config.set(section, 'Type', 'Application')81 config.set(section, 'Type', 'Application')
@@ -99,6 +100,8 @@
99100
100 def prime_application_restart(self, app_id: str, cmd: str) -> None:101 def prime_application_restart(self, app_id: str, cmd: str) -> None:
101 filename = self.get_desktop_filename(app_id)102 filename = self.get_desktop_filename(app_id)
103 if self.app_terminal:
104 cmd += ';$SHELL'
102 self.config.set('Desktop Entry', 'Exec', cmd)105 self.config.set('Desktop Entry', 'Exec', cmd)
103 os.makedirs(os.path.dirname(filename), exist_ok=True)106 os.makedirs(os.path.dirname(filename), exist_ok=True)
104 with open(filename, 'wt') as stream:107 with open(filename, 'wt') as stream:

Subscribers

People subscribed via source and target branches