Merge lp:~sylvain-pineau/checkbox/fix-1577831 into lp:checkbox
- fix-1577831
- Merge into trunk
Proposed by
Sylvain Pineau
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Sylvain Pineau | ||||
Approved revision: | 4355 | ||||
Merged at revision: | 4366 | ||||
Proposed branch: | lp:~sylvain-pineau/checkbox/fix-1577831 | ||||
Merge into: | lp:checkbox | ||||
Diff against target: |
657 lines (+17/-603) 3 files modified
checkbox-ng/launchers/checkbox-cli (+4/-3) checkbox-ng/launchers/checkbox-cli2 (+0/-596) plainbox/plainbox/impl/session/restart.py (+13/-4) |
||||
To merge this branch: | bzr merge lp:~sylvain-pineau/checkbox/fix-1577831 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Larson | Approve | ||
Sylvain Pineau (community) | Needs Resubmitting | ||
Review via email: mp+294903@code.launchpad.net |
Commit message
Description of the change
Fixes the linked bug, updating the snappy restart strategy to comply with s16 and kill checkbox-cli2 then just requesting a binary called checkbox-cli in the snapcraft.yaml (less binaries is less confusing for end-users)
To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote : | # |
review:
Needs Resubmitting
Revision history for this message
Paul Larson (pwlars) wrote : | # |
One really minor comment below, feel free to take it or leave it. Otherwise looks fine to me, and love the removeal of the extra launcher with the name that didn't make it's purpose clear :)
review:
Approve
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote : | # |
Thanks for the review, adiós checkbox-cli2
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'checkbox-ng/launchers/checkbox-cli' |
2 | --- checkbox-ng/launchers/checkbox-cli 2016-05-18 08:38:44 +0000 |
3 | +++ checkbox-ng/launchers/checkbox-cli 2016-05-19 19:44:13 +0000 |
4 | @@ -717,10 +717,11 @@ |
5 | 'submissions/submit/') |
6 | secure_id = self.launcher.transports[transport].get( |
7 | 'secure_id', None) |
8 | - if not secure_id: |
9 | + if not secure_id and self.is_interactive: |
10 | secure_id = input(self.C.BLUE(_('Enter secure-id:'))) |
11 | - options = "secure_id={}".format(secure_id) |
12 | - self.transports[transport] = cls(url, options) |
13 | + if secure_id: |
14 | + options = "secure_id={}".format(secure_id) |
15 | + self.transports[transport] = cls(url, options) |
16 | |
17 | def _export_results(self): |
18 | for report in self.launcher.stock_reports: |
19 | |
20 | === removed file 'checkbox-ng/launchers/checkbox-cli2' |
21 | --- checkbox-ng/launchers/checkbox-cli2 2015-12-14 18:15:22 +0000 |
22 | +++ checkbox-ng/launchers/checkbox-cli2 1970-01-01 00:00:00 +0000 |
23 | @@ -1,596 +0,0 @@ |
24 | -#!/usr/bin/env python3 |
25 | -# This file is part of Checkbox. |
26 | -# |
27 | -# Copyright 2015 Canonical Ltd. |
28 | -# Written by: |
29 | -# Sylvain Pineau <sylvain.pineau@canonical.com> |
30 | -# |
31 | -# Checkbox is free software: you can redistribute it and/or modify |
32 | -# it under the terms of the GNU General Public License version 3, |
33 | -# as published by the Free Software Foundation. |
34 | -# |
35 | -# Checkbox is distributed in the hope that it will be useful, |
36 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
37 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
38 | -# GNU General Public License for more details. |
39 | -# |
40 | -# You should have received a copy of the GNU General Public License |
41 | -# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
42 | - |
43 | -""" |
44 | -Checkbox CLI Application. |
45 | - |
46 | -WARNING: this is not a launcher interpreter. |
47 | -""" |
48 | - |
49 | -from argparse import SUPPRESS |
50 | -from shutil import copyfileobj |
51 | -import gettext |
52 | -import io |
53 | -import json |
54 | -import logging |
55 | -import os |
56 | -import sys |
57 | - |
58 | -from guacamole import Command |
59 | -from guacamole.core import Ingredient |
60 | -from guacamole.ingredients import ansi |
61 | -from guacamole.ingredients import argparse |
62 | -from guacamole.ingredients import cmdtree |
63 | -from guacamole.recipes.cmd import CommandRecipe |
64 | - |
65 | -# TODO: use public APIs here |
66 | -from plainbox.abc import IJobResult |
67 | -from plainbox.i18n import ngettext |
68 | -from plainbox.i18n import pgettext as C_ |
69 | -from plainbox.impl.commands.inv_run import seconds_to_human_duration |
70 | -from plainbox.impl.commands.inv_run import Action |
71 | -from plainbox.impl.commands.inv_run import ActionUI |
72 | -from plainbox.impl.commands.inv_run import NormalUI |
73 | -from plainbox.impl.commands.inv_run import ReRunJob |
74 | -from plainbox.impl.color import Colorizer |
75 | -from plainbox.impl.exporter import ByteStringStreamTranslator |
76 | -from plainbox.impl.ingredients import CanonicalCrashIngredient |
77 | -from plainbox.impl.ingredients import RenderingContextIngredient |
78 | -from plainbox.impl.ingredients import SessionAssistantIngredient |
79 | -from plainbox.impl.result import tr_outcome |
80 | -from plainbox.impl.result import JobResultBuilder |
81 | -from plainbox.impl.result import MemoryJobResult |
82 | -from plainbox.impl.session.assistant import SA_RESTARTABLE |
83 | -from plainbox.impl.session.jobs import InhibitionCause |
84 | -from plainbox.vendor.textland import get_display |
85 | - |
86 | -from checkbox_ng.misc import SelectableJobTreeNode |
87 | -from checkbox_ng.ui import ScrollableTreeNode |
88 | -from checkbox_ng.ui import ShowMenu |
89 | -from checkbox_ng.ui import ShowRerun |
90 | - |
91 | - |
92 | -_ = gettext.gettext |
93 | - |
94 | -_logger = logging.getLogger("checkbox-cli") |
95 | - |
96 | - |
97 | -class DisplayIngredient(Ingredient): |
98 | - |
99 | - """Ingredient that adds a Textland display to guacamole.""" |
100 | - |
101 | - def late_init(self, context): |
102 | - """Add a DisplayIngredient as ``display`` to the guacamole context.""" |
103 | - context.display = get_display() |
104 | - |
105 | - |
106 | -class CheckboxCommandRecipe(CommandRecipe): |
107 | - |
108 | - """A recipe for using Checkbox-enhanced commands.""" |
109 | - |
110 | - def get_ingredients(self): |
111 | - """Get a list of ingredients for guacamole.""" |
112 | - return [ |
113 | - cmdtree.CommandTreeBuilder(self.command), |
114 | - cmdtree.CommandTreeDispatcher(), |
115 | - argparse.ParserIngredient(), |
116 | - CanonicalCrashIngredient(), |
117 | - ansi.ANSIIngredient(), |
118 | - SessionAssistantIngredient(), |
119 | - RenderingContextIngredient(), |
120 | - DisplayIngredient() |
121 | - ] |
122 | - |
123 | - |
124 | -class CheckboxUI(NormalUI): |
125 | - |
126 | - def considering_job(self, job, job_state): |
127 | - pass |
128 | - |
129 | - |
130 | -class CheckboxCommand(Command): |
131 | - |
132 | - """ |
133 | - A command with Checkbox-enhanced ingredients. |
134 | - |
135 | - This command has two additional items in the guacamole execution context, |
136 | - the :class:`DisplayIngredient` object ``display`` and the |
137 | - :class:`SessionAssistant` object ``sa``. |
138 | - """ |
139 | - |
140 | - bug_report_url = "https://bugs.launchpad.net/checkbox-ng/+filebug" |
141 | - |
142 | - def main(self, argv=None, exit=True): |
143 | - """ |
144 | - Shortcut for running a command. |
145 | - |
146 | - See :meth:`guacamole.recipes.Recipe.main()` for details. |
147 | - """ |
148 | - return CheckboxCommandRecipe(self).main(argv, exit) |
149 | - |
150 | - |
151 | -class checkbox_cli(CheckboxCommand): |
152 | - |
153 | - """Tool to run Checkbox jobs interactively from the command line.""" |
154 | - |
155 | - app_id = 'checkbox-cli' |
156 | - |
157 | - def get_sa_api_version(self): |
158 | - return '0.99' |
159 | - |
160 | - def get_sa_api_flags(self): |
161 | - return (SA_RESTARTABLE,) |
162 | - |
163 | - def register_arguments(self, parser): |
164 | - """Method called to register command line arguments.""" |
165 | - parser.add_argument( |
166 | - '-t', '--test-plan', action="store", metavar=_("TEST-PLAN-ID"), |
167 | - default=None, |
168 | - # TRANSLATORS: this is in imperative form |
169 | - help=_("load the specified test plan")) |
170 | - parser.add_argument( |
171 | - '--secure_id', metavar="SECURE_ID", |
172 | - help=_("Canonical hardware identifier (optional)")) |
173 | - parser.add_argument( |
174 | - '--non-interactive', action='store_true', |
175 | - help=_("skip tests that require interactivity")) |
176 | - parser.add_argument( |
177 | - '--dont-suppress-output', action="store_true", default=False, |
178 | - help=_("don't suppress the output of certain job plugin types")) |
179 | - parser.add_argument( |
180 | - '--staging', action='store_true', default=False, |
181 | - # Hide staging from help message (See pad.lv/1350005) |
182 | - help=SUPPRESS) |
183 | - parser.add_argument( |
184 | - '--resume', dest='session_id', metavar="SESSION_ID", |
185 | - help=SUPPRESS) |
186 | - |
187 | - def invoked(self, ctx): |
188 | - """Method called when the command is invoked.""" |
189 | - self.ctx = ctx |
190 | - self.transport = self._create_transport() |
191 | - self.C = Colorizer() |
192 | - try: |
193 | - self._do_normal_sequence() |
194 | - self._export_results() |
195 | - if ctx.args.secure_id: |
196 | - self._send_results() |
197 | - ctx.sa.finalize_session() |
198 | - except KeyboardInterrupt: |
199 | - return 1 |
200 | - |
201 | - def _export_results(self): |
202 | - if self.is_interactive: |
203 | - print(self.C.header(_("Results"))) |
204 | - # This requires a bit more finesse, as exporters output bytes |
205 | - # and stdout needs a string. |
206 | - translating_stream = ByteStringStreamTranslator( |
207 | - sys.stdout, "utf-8") |
208 | - self.ctx.sa.export_to_stream( |
209 | - '2013.com.canonical.plainbox::text', (), translating_stream) |
210 | - base_dir = os.path.join( |
211 | - os.getenv( |
212 | - 'XDG_DATA_HOME', os.path.expanduser("~/.local/share/")), |
213 | - "checkbox-ng") |
214 | - if not os.path.exists(base_dir): |
215 | - os.makedirs(base_dir) |
216 | - exp_options = ['with-sys-info', 'with-summary', 'with-job-description', |
217 | - 'with-text-attachments', 'with-certification-status', |
218 | - 'with-job-defs', 'with-io-log', 'with-comments'] |
219 | - exporters = [ |
220 | - '2013.com.canonical.plainbox::hexr', |
221 | - '2013.com.canonical.plainbox::html', |
222 | - '2013.com.canonical.plainbox::xlsx', |
223 | - '2013.com.canonical.plainbox::json', |
224 | - ] |
225 | - print() |
226 | - for unit_name in exporters: |
227 | - results_path = self.ctx.sa.export_to_file(unit_name, exp_options, |
228 | - base_dir) |
229 | - print("file://{}".format(results_path)) |
230 | - |
231 | - def _send_results(self): |
232 | - print() |
233 | - print(_("Sending hardware report to Canonical Certification")) |
234 | - print(_("Server URL is: {0}").format(self.transport.url)) |
235 | - result = self.ctx.sa.export_to_transport( |
236 | - "2013.com.canonical.plainbox::hexr", self.transport) |
237 | - if 'url' in result: |
238 | - print(result['url']) |
239 | - |
240 | - def _create_transport(self): |
241 | - if self.ctx.args.secure_id: |
242 | - return self.ctx.sa.get_canonical_certification_transport( |
243 | - self.ctx.args.secure_id, staging=self.ctx.args.staging) |
244 | - |
245 | - def _get_interactively_picked_testplans(self): |
246 | - test_plan_ids = self.ctx.sa.get_test_plans() |
247 | - test_plan_names = [self.ctx.sa.get_test_plan(tp_id).name for tp_id in |
248 | - test_plan_ids] |
249 | - try: |
250 | - selected_index = self.ctx.display.run( |
251 | - ShowMenu(_("Select test plan"), |
252 | - test_plan_names, [], |
253 | - multiple_allowed=False))[0] |
254 | - except IndexError: |
255 | - return None |
256 | - return test_plan_ids[selected_index] |
257 | - |
258 | - def _interactively_pick_jobs_to_run(self): |
259 | - job_list = [self.ctx.sa.get_job(job_id) for job_id in |
260 | - self.ctx.sa.get_static_todo_list()] |
261 | - tree = SelectableJobTreeNode.create_simple_tree(self.ctx.sa, job_list) |
262 | - title = _('Choose tests to run on your system:') |
263 | - self.ctx.display.run(ScrollableTreeNode(tree, title)) |
264 | - # NOTE: tree.selection is correct but ordered badly. To retain |
265 | - # the original ordering we should just treat it as a mask and |
266 | - # use it to filter jobs from get_static_todo_list. |
267 | - wanted_set = frozenset([job.id for job in tree.selection]) |
268 | - job_id_list = [job_id for job_id in self.ctx.sa.get_static_todo_list() |
269 | - if job_id in wanted_set] |
270 | - self.ctx.sa.use_alternate_selection(job_id_list) |
271 | - |
272 | - @property |
273 | - def is_interactive(self): |
274 | - """ |
275 | - Flag indicating that this is an interactive invocation. |
276 | - |
277 | - We can then interact with the user when we encounter OUTCOME_UNDECIDED. |
278 | - """ |
279 | - return (sys.stdin.isatty() and sys.stdout.isatty() and not |
280 | - self.ctx.args.non_interactive) |
281 | - |
282 | - def _get_ui_for_job(self, job): |
283 | - if self.ctx.args.dont_suppress_output is False and job.plugin in ( |
284 | - 'local', 'resource', 'attachment'): |
285 | - return CheckboxUI(self.C.c, show_cmd_output=False) |
286 | - else: |
287 | - return CheckboxUI(self.C.c, show_cmd_output=True) |
288 | - |
289 | - def _run_single_job_with_ui_loop(self, job, ui): |
290 | - print(self.C.header(job.tr_summary(), fill='-')) |
291 | - print(_("ID: {0}").format(job.id)) |
292 | - print(_("Category: {0}").format( |
293 | - self.ctx.sa.get_job_state(job.id).effective_category_id)) |
294 | - comments = "" |
295 | - while True: |
296 | - if job.plugin in ('user-interact', 'user-interact-verify', |
297 | - 'user-verify', 'manual'): |
298 | - ui.notify_about_purpose(job) |
299 | - if (self.is_interactive and |
300 | - job.plugin in ('user-interact', |
301 | - 'user-interact-verify', |
302 | - 'manual')): |
303 | - ui.notify_about_steps(job) |
304 | - if job.plugin == 'manual': |
305 | - cmd = 'run' |
306 | - else: |
307 | - cmd = ui.wait_for_interaction_prompt(job) |
308 | - if cmd == 'run' or cmd is None: |
309 | - result_builder = self.ctx.sa.run_job(job.id, ui, False) |
310 | - elif cmd == 'comment': |
311 | - new_comment = input(self.C.BLUE( |
312 | - _('Please enter your comments:') + '\n')) |
313 | - if new_comment: |
314 | - comments += new_comment + '\n' |
315 | - continue |
316 | - elif cmd == 'skip': |
317 | - result_builder = JobResultBuilder( |
318 | - outcome=IJobResult.OUTCOME_SKIP, |
319 | - comments=_("Explicitly skipped before" |
320 | - " execution")) |
321 | - if comments != "": |
322 | - result_builder.comments = comments |
323 | - break |
324 | - elif cmd == 'quit': |
325 | - raise SystemExit() |
326 | - else: |
327 | - result_builder = self.ctx.sa.run_job(job.id, ui, False) |
328 | - else: |
329 | - if 'noreturn' in job.get_flag_set(): |
330 | - ui.noreturn_job() |
331 | - result_builder = self.ctx.sa.run_job(job.id, ui, False) |
332 | - if (self.is_interactive and |
333 | - result_builder.outcome == IJobResult.OUTCOME_UNDECIDED): |
334 | - try: |
335 | - if comments != "": |
336 | - result_builder.comments = comments |
337 | - ui.notify_about_verification(job) |
338 | - self._interaction_callback(job, result_builder) |
339 | - except ReRunJob: |
340 | - self.ctx.sa.use_job_result(job.id, |
341 | - result_builder.get_result()) |
342 | - continue |
343 | - break |
344 | - return result_builder |
345 | - |
346 | - def _pick_action_cmd(self, action_list, prompt=None): |
347 | - return ActionUI(action_list, prompt).run() |
348 | - |
349 | - def _interaction_callback(self, job, result_builder, |
350 | - prompt=None, allowed_outcome=None): |
351 | - result = result_builder.get_result() |
352 | - if prompt is None: |
353 | - prompt = _("Select an outcome or an action: ") |
354 | - if allowed_outcome is None: |
355 | - allowed_outcome = [IJobResult.OUTCOME_PASS, |
356 | - IJobResult.OUTCOME_FAIL, |
357 | - IJobResult.OUTCOME_SKIP] |
358 | - allowed_actions = [ |
359 | - Action('c', _('add a comment'), 'set-comments') |
360 | - ] |
361 | - if IJobResult.OUTCOME_PASS in allowed_outcome: |
362 | - allowed_actions.append( |
363 | - Action('p', _('set outcome to {0}').format( |
364 | - self.C.GREEN(C_('set outcome to <pass>', 'pass'))), |
365 | - 'set-pass')) |
366 | - if IJobResult.OUTCOME_FAIL in allowed_outcome: |
367 | - allowed_actions.append( |
368 | - Action('f', _('set outcome to {0}').format( |
369 | - self.C.RED(C_('set outcome to <fail>', 'fail'))), |
370 | - 'set-fail')) |
371 | - if IJobResult.OUTCOME_SKIP in allowed_outcome: |
372 | - allowed_actions.append( |
373 | - Action('s', _('set outcome to {0}').format( |
374 | - self.C.YELLOW(C_('set outcome to <skip>', 'skip'))), |
375 | - 'set-skip')) |
376 | - if job.command is not None: |
377 | - allowed_actions.append( |
378 | - Action('r', _('re-run this job'), 're-run')) |
379 | - if result.return_code is not None: |
380 | - if result.return_code == 0: |
381 | - suggested_outcome = IJobResult.OUTCOME_PASS |
382 | - else: |
383 | - suggested_outcome = IJobResult.OUTCOME_FAIL |
384 | - allowed_actions.append( |
385 | - Action('', _('set suggested outcome [{0}]').format( |
386 | - tr_outcome(suggested_outcome)), 'set-suggested')) |
387 | - while result.outcome not in allowed_outcome: |
388 | - print(_("Please decide what to do next:")) |
389 | - print(" " + _("outcome") + ": {0}".format( |
390 | - self.C.result(result))) |
391 | - if result.comments is None: |
392 | - print(" " + _("comments") + ": {0}".format( |
393 | - C_("none comment", "none"))) |
394 | - else: |
395 | - print(" " + _("comments") + ": {0}".format( |
396 | - self.C.CYAN(result.comments, bright=False))) |
397 | - cmd = self._pick_action_cmd(allowed_actions) |
398 | - if cmd == 'set-pass': |
399 | - result_builder.outcome = IJobResult.OUTCOME_PASS |
400 | - elif cmd == 'set-fail': |
401 | - result_builder.outcome = IJobResult.OUTCOME_FAIL |
402 | - elif cmd == 'set-skip' or cmd is None: |
403 | - result_builder.outcome = IJobResult.OUTCOME_SKIP |
404 | - elif cmd == 'set-suggested': |
405 | - result_builder.outcome = suggested_outcome |
406 | - elif cmd == 'set-comments': |
407 | - new_comment = input(self.C.BLUE( |
408 | - _('Please enter your comments:') + '\n')) |
409 | - if new_comment: |
410 | - result_builder.add_comment(new_comment) |
411 | - elif cmd == 're-run': |
412 | - raise ReRunJob |
413 | - result = result_builder.get_result() |
414 | - |
415 | - def _run_jobs(self, jobs_to_run): |
416 | - estimated_time = 0 |
417 | - for job_id in jobs_to_run: |
418 | - job = self.ctx.sa.get_job(job_id) |
419 | - if (job.estimated_duration is not None |
420 | - and estimated_time is not None): |
421 | - estimated_time += job.estimated_duration |
422 | - else: |
423 | - estimated_time = None |
424 | - for job_no, job_id in enumerate(jobs_to_run, start=1): |
425 | - print(self.C.header( |
426 | - _('Running job {} / {}. Estimated time left: {}').format( |
427 | - job_no, len(jobs_to_run), |
428 | - seconds_to_human_duration(max(0, estimated_time)) |
429 | - if estimated_time is not None else _("unknown")), |
430 | - fill='-')) |
431 | - job = self.ctx.sa.get_job(job_id) |
432 | - builder = self._run_single_job_with_ui_loop( |
433 | - job, self._get_ui_for_job(job)) |
434 | - result = builder.get_result() |
435 | - self.ctx.sa.use_job_result(job_id, result) |
436 | - if (job.estimated_duration is not None |
437 | - and estimated_time is not None): |
438 | - estimated_time -= job.estimated_duration |
439 | - |
440 | - def _get_rerun_candidates(self): |
441 | - """Get all the tests that might be selected for rerunning.""" |
442 | - def rerun_predicate(job_state): |
443 | - return job_state.result.outcome in ( |
444 | - IJobResult.OUTCOME_FAIL, IJobResult.OUTCOME_CRASH, |
445 | - IJobResult.OUTCOME_NOT_SUPPORTED, IJobResult.OUTCOME_SKIP) |
446 | - rerun_candidates = [] |
447 | - todo_list = self.ctx.sa.get_static_todo_list() |
448 | - job_states = {job_id: self.ctx.sa.get_job_state(job_id) for job_id |
449 | - in todo_list} |
450 | - for job_id, job_state in job_states.items(): |
451 | - if rerun_predicate(job_state): |
452 | - rerun_candidates.append(self.ctx.sa.get_job(job_id)) |
453 | - return rerun_candidates |
454 | - |
455 | - def _maybe_rerun_jobs(self): |
456 | - # create a list of jobs that qualify for rerunning |
457 | - rerun_candidates = self._get_rerun_candidates() |
458 | - # bail-out early if no job qualifies for rerunning |
459 | - if not rerun_candidates: |
460 | - return False |
461 | - tree = SelectableJobTreeNode.create_simple_tree(self.ctx.sa, |
462 | - rerun_candidates) |
463 | - # nothing to select in root node and categories - bailing out |
464 | - if not tree.jobs and not tree._categories: |
465 | - return False |
466 | - # deselect all by default |
467 | - tree.set_descendants_state(False) |
468 | - self.ctx.display.run(ShowRerun(tree, _("Select jobs to re-run"))) |
469 | - wanted_set = frozenset(tree.selection) |
470 | - if not wanted_set: |
471 | - # nothing selected - nothing to run |
472 | - return False |
473 | - rerun_candidates = [] |
474 | - # include resource jobs that selected jobs depend on |
475 | - resources_to_rerun = [] |
476 | - for job in wanted_set: |
477 | - job_state = self.ctx.sa.get_job_state(job.id) |
478 | - for inhibitor in job_state.readiness_inhibitor_list: |
479 | - if inhibitor.cause == InhibitionCause.FAILED_DEP: |
480 | - resources_to_rerun.append(inhibitor.related_job) |
481 | - # reset outcome of jobs that are selected for re-running |
482 | - for job in list(wanted_set) + resources_to_rerun: |
483 | - self.ctx.sa.get_job_state(job.id).result = MemoryJobResult({}) |
484 | - rerun_candidates.append(job.id) |
485 | - self._run_jobs(rerun_candidates) |
486 | - return True |
487 | - |
488 | - def _do_normal_sequence(self): |
489 | - self.ctx.sa.select_providers("*") |
490 | - self.ctx.sa.configure_application_restart( |
491 | - lambda session_id: [ |
492 | - 'sh', '-c', ' '.join([ |
493 | - os.path.abspath(__file__), |
494 | - "--resume", session_id]) |
495 | - ]) |
496 | - resumed = self._maybe_resume_session() |
497 | - if not resumed: |
498 | - print(_("Preparing...")) |
499 | - self.ctx.sa.start_new_session(_("Checkbox CLI Session")) |
500 | - testplan_id = None |
501 | - if self.ctx.args.test_plan: |
502 | - if self.ctx.args.test_plan in self.ctx.sa.get_test_plans(): |
503 | - testplan_id = self.ctx.args.test_plan |
504 | - elif self.is_interactive: |
505 | - testplan_id = self._get_interactively_picked_testplans() |
506 | - if not testplan_id: |
507 | - self.ctx.rc.reset() |
508 | - self.ctx.rc.bg = 'red' |
509 | - self.ctx.rc.fg = 'bright_white' |
510 | - self.ctx.rc.bold = 1 |
511 | - self.ctx.rc.para(_("Test plan not found!")) |
512 | - raise SystemExit(1) |
513 | - self.ctx.sa.select_test_plan(testplan_id) |
514 | - self.ctx.sa.update_app_blob(json.dumps( |
515 | - {'testplan_id': testplan_id, }).encode("UTF-8")) |
516 | - self.ctx.sa.bootstrap() |
517 | - if self.is_interactive: |
518 | - self._interactively_pick_jobs_to_run() |
519 | - self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) |
520 | - if self.is_interactive: |
521 | - while True: |
522 | - if self._maybe_rerun_jobs(): |
523 | - continue |
524 | - else: |
525 | - break |
526 | - |
527 | - def _handle_last_job_after_resume(self, metadata): |
528 | - last_job = metadata.running_job_name |
529 | - if last_job is None: |
530 | - return |
531 | - print(_("Previous session run tried to execute job: {}").format( |
532 | - last_job)) |
533 | - cmd = self._pick_action_cmd([ |
534 | - Action('s', _("skip that job"), 'skip'), |
535 | - Action('p', _("mark it as passed and continue"), 'pass'), |
536 | - Action('f', _("mark it as failed and continue"), 'fail'), |
537 | - Action('r', _("run it again"), 'run'), |
538 | - ], _("What do you want to do with that job?")) |
539 | - if cmd == 'skip' or cmd is None: |
540 | - result = MemoryJobResult({ |
541 | - 'outcome': IJobResult.OUTCOME_SKIP, |
542 | - 'comments': _("Skipped after resuming execution") |
543 | - }) |
544 | - elif cmd == 'pass': |
545 | - result = MemoryJobResult({ |
546 | - 'outcome': IJobResult.OUTCOME_PASS, |
547 | - 'comments': _("Passed after resuming execution") |
548 | - }) |
549 | - elif cmd == 'fail': |
550 | - result = MemoryJobResult({ |
551 | - 'outcome': IJobResult.OUTCOME_FAIL, |
552 | - 'comments': _("Failed after resuming execution") |
553 | - }) |
554 | - elif cmd == 'run': |
555 | - result = None |
556 | - if result: |
557 | - self.ctx.sa.use_job_result(last_job, result) |
558 | - |
559 | - def _maybe_resume_session(self): |
560 | - # Try to use the first session that can be resumed if the user agrees |
561 | - resume_candidates = list(self.ctx.sa.get_resumable_sessions()) |
562 | - resumed = False |
563 | - if resume_candidates: |
564 | - if self.ctx.args.session_id: |
565 | - for candidate in resume_candidates: |
566 | - if candidate.id == self.ctx.args.session_id: |
567 | - resume_candidates = (candidate, ) |
568 | - break |
569 | - else: |
570 | - raise RuntimeError("Requested session is not resumable!") |
571 | - elif self.is_interactive: |
572 | - print(self.C.header(_("Resume Incomplete Session"))) |
573 | - print(ngettext( |
574 | - "There is {0} incomplete session that might be resumed", |
575 | - "There are {0} incomplete sessions that might be resumed", |
576 | - len(resume_candidates) |
577 | - ).format(len(resume_candidates))) |
578 | - else: |
579 | - return |
580 | - for candidate in resume_candidates: |
581 | - if self.ctx.args.session_id: |
582 | - cmd = 'resume' |
583 | - else: |
584 | - # Skip sessions that the user doesn't want to resume |
585 | - cmd = self._pick_action_cmd([ |
586 | - Action('r', _("resume this session"), 'resume'), |
587 | - Action('n', _("next session"), 'next'), |
588 | - Action('c', _("create new session"), 'create') |
589 | - ], _("Do you want to resume session {0!a}?").format( |
590 | - candidate.id)) |
591 | - if cmd == 'next': |
592 | - continue |
593 | - elif cmd == 'create' or cmd is None: |
594 | - break |
595 | - elif cmd == 'resume': |
596 | - metadata = self.ctx.sa.resume_session(candidate.id) |
597 | - app_blob = json.loads(metadata.app_blob.decode("UTF-8")) |
598 | - test_plan_id = app_blob['testplan_id'] |
599 | - # FIXME selecting again the testplan on resume resets both |
600 | - # the static and dynamic todo lists. |
601 | - # We're then saving the selection from the saved run_list |
602 | - # by accessing the session private context object. |
603 | - selected_id_list = [job.id for job in |
604 | - self.ctx.sa._context.state.run_list] |
605 | - self.ctx.sa.select_test_plan(test_plan_id) |
606 | - self.ctx.sa.bootstrap() |
607 | - self.ctx.sa.use_alternate_selection(selected_id_list) |
608 | - # If we resumed maybe not rerun the same, probably broken |
609 | - # job |
610 | - self._handle_last_job_after_resume(metadata) |
611 | - self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) |
612 | - resumed = True |
613 | - # Finally ignore other sessions that can be resumed |
614 | - break |
615 | - return resumed |
616 | - |
617 | - |
618 | -if __name__ == '__main__': |
619 | - checkbox_cli().main() |
620 | |
621 | === modified file 'plainbox/plainbox/impl/session/restart.py' |
622 | --- plainbox/plainbox/impl/session/restart.py 2016-05-04 09:21:57 +0000 |
623 | +++ plainbox/plainbox/impl/session/restart.py 2016-05-19 19:44:13 +0000 |
624 | @@ -22,6 +22,7 @@ |
625 | import abc |
626 | import errno |
627 | import os |
628 | +import shlex |
629 | import subprocess |
630 | |
631 | from plainbox.impl.secure.config import PlainBoxConfigParser |
632 | @@ -161,13 +162,21 @@ |
633 | In this stategy plainbox will create and enable a systemd unit that |
634 | will be run when the OS resumes. |
635 | """ |
636 | + cmd = shlex.split(cmd)[0] |
637 | snap_name = os.getenv('SNAP_NAME') |
638 | + data_path = os.getenv('SNAP_DATA') |
639 | + base_dir = 'snap' |
640 | + if os.getenv("SNAP_APP_PATH"): |
641 | + data_path = os.getenv('SNAP_APP_DATA_PATH') |
642 | + base_dir = 'apps' |
643 | # NOTE: This implies that any snap wishing to include a Checkbox |
644 | # application to be autostarted creates snapcraft binary |
645 | - # called "checkbox-autostart" |
646 | - self.config.set('Service', 'ExecStart', |
647 | - '/apps/bin/{}.checkbox-autostart --resume {}'.format( |
648 | - snap_name, session_id)) |
649 | + # called "checkbox-cli" |
650 | + binary_name = '/{}/bin/{}.checkbox-cli'.format(base_dir, snap_name) |
651 | + self.config.set('Service', 'Environment', |
652 | + '"PLAINBOX_SESSION_REPOSITORY={}"'.format(data_path)) |
653 | + self.config.set('Service', 'ExecStart', '{} {}'.format( |
654 | + binary_name, ' '.join(cmd.split()[1:]))) |
655 | filename = self.get_autostart_config_filename() |
656 | os.makedirs(os.path.dirname(filename), exist_ok=True) |
657 | with open(filename, 'wt') as stream: |
Ready for review, heavily tested on a snappy vivid system running stress tests reboot loops.