Merge ~sylvain-pineau/checkbox-ng:urwid_manifest into checkbox-ng:master

Proposed by Sylvain Pineau
Status: Rejected
Rejected by: Sylvain Pineau
Proposed branch: ~sylvain-pineau/checkbox-ng:urwid_manifest
Merge into: checkbox-ng:master
Diff against target: 537 lines (+343/-2)
8 files modified
checkbox_ng/launcher/master.py (+14/-1)
checkbox_ng/launcher/subcommands.py (+12/-0)
checkbox_ng/urwid_ui.py (+131/-0)
docs/launcher-tutorial.rst (+20/-0)
plainbox/impl/launcher.py (+2/-0)
plainbox/impl/resource.py (+54/-0)
plainbox/impl/session/assistant.py (+104/-1)
plainbox/impl/session/remote_assistant.py (+6/-0)
Reviewer Review Type Date Requested Status
Devices Certification Bot Needs Fixing
Sylvain Pineau (community) Needs Fixing
Jonathan Cave (community) Approve
Maciej Kisielewski (community) Needs Fixing
Review via email: mp+375401@code.launchpad.net

Description of the change

Manifest V2 (backward compatible with existing test plans, collect manifest will be automatically excluded from run list)

Tested with the following variants:

$ checkbox-cli
$ checkbox-cli launcher
$ checkbox-cli ./mylauncher
$ checkbox-cli master 127.0.0.1 ./mylauncher
$ checkbox-cli master 127.0.0.1
$ checkbox-cli master 192.168.1.47 ./mylauncher
$ checkbox-cli master 192.168.1.47

w/ and w/o an existing ~/.local/share/plainbox/machine-manifest.json

using this type of launcher:

[launcher]
app_id = com.canonical.certification:checkbox-test
launcher_version = 1
stock_reports = text, submission_files

[test plan]
unit = com.canonical.certification::touchscreen-cert-manual
forced = yes

[test selection]
forced = yes

[manifest]
com.canonical.certification::has_touchscreen = no
com.canonical.certification::has_usb_type_c = true
com.canonical.certification::test_enum = 418

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

URWID review:

The new urwid screen is very difficult to use.
It took me solid one minute to figure out you have to use left/right arrows and a space-bar.
For a screen with one manifest option, there's no cursor movement, giving an impression that the program is frozen.
The '-' in front of the manifest option is misleading (on other screens it means unfolded tree).
I would prefer radio buttons to be on the left of the screen (or anywhere nearby to the label) - on panoramic screens it's easy to miss (happened to me the first time I tried it).
I think tooltip in the footer is necessary to guide the operator.
I would like a shortcut to select options (like y/n), so I could quickly go through the list.
I don't get why do we need two radio buttons for boolean variables. Why not use checkboxes as we do on other screens? Simpler code, simpler UX, etc.

Remote review:
- branch is not based on a fresh master, so testing remote as-is can by crashy, but I found that it can be rebased on master cleanly
- there's no distinct interaction step on the manifest screen, breaking the remote connection on that screen rolls the session back to test selection screen (just noticing, I'm not bothered by it)
- new RemoteSA method call means API bump is necessary

Code review:
Pleasant read. Some codes below.

review: Needs Fixing
Revision history for this message
Jonathan Cave (jocave) wrote :

Really like this, I think it is a big improvement over the existing implementation already.

A few of the usability tweaks Maciej mentions could elevate it even further, although I personally think the two radio buttons works in this case. The tweaks I would like are mostly the shortcut related functionality:
 - shortcut keys y/n for selection of bool entries
 - hitting y/n advances the active entry i.e. drops it down a line
 - some kind of surround for the edit widget on natural entries to make them easier to spot
     square brackets could be enough: [ 123 ]
 - hitting return on natural entries advances the active entry

This means that for quick completion of e.g.:
  bool
  bool
  natural
  bool
you could hit:
  y n 123 enter y t

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

just rebased on master to include the latest commits (and resolving conflicts around chosen_jobs).

Thanks for the shortcuts ideas, I'll add them to the unhandled_input urwid method(s).

Original comments can still be visible if you select a the first version in the Preview Diff dropdown list below.

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

resubmitting after another rebase

review: Needs Resubmitting
Revision history for this message
Jonathan Cave (jocave) wrote :

My only objections are UI imperfections which I do not think should block landing the improvement in overall functionality.

review: Approve
Revision history for this message
Devices Certification Bot (ce-certification-qa) wrote :

I tried to merge it but there are some problems. Typically you want to merge or rebase and try again.

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

I tried to merge it but there are some problems. Typically you want to merge or rebase and try again.

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

I tried to merge it but there are some problems. Typically you want to merge or rebase and try again.

review: Needs Fixing
Revision history for this message
Devices Certification Bot (ce-certification-qa) wrote :

I tried to merge it but there are some problems. Typically you want to merge or rebase and try again.

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

Unmerged commits

d7ad6d6... by Sylvain Pineau

master: Save the launcher manifest section on disk when test_selection_forced is set

d5da540... by Sylvain Pineau

master: Add the system manifest urwird step

507efcb... by Sylvain Pineau

subcommands:launcher: Save the launcher manifest section on disk when test_selection_forced is set

05b98b1... by Sylvain Pineau

subcommands:launcher: Add the system manifest urwird step

cc4bdae... by Sylvain Pineau

urwid: Add the ManifestBrowser screen

6a355c4... by Sylvain Pineau

session:remote_assistant: Add both get_manifest_repr and save_manifest

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py
index 5c9afcf..2107fbf 100644
--- a/checkbox_ng/launcher/master.py
+++ b/checkbox_ng/launcher/master.py
@@ -36,11 +36,13 @@ from tempfile import SpooledTemporaryFile
3636
37from plainbox.impl.color import Colorizer37from plainbox.impl.color import Colorizer
38from plainbox.impl.launcher import DefaultLauncherDefinition38from plainbox.impl.launcher import DefaultLauncherDefinition
39from plainbox.impl.secure.config import Unset
39from plainbox.impl.secure.sudo_broker import SudoProvider40from plainbox.impl.secure.sudo_broker import SudoProvider
40from plainbox.impl.session.remote_assistant import RemoteSessionAssistant41from plainbox.impl.session.remote_assistant import RemoteSessionAssistant
41from plainbox.vendor import rpyc42from plainbox.vendor import rpyc
42from checkbox_ng.urwid_ui import TestPlanBrowser43from checkbox_ng.urwid_ui import TestPlanBrowser
43from checkbox_ng.urwid_ui import CategoryBrowser44from checkbox_ng.urwid_ui import CategoryBrowser
45from checkbox_ng.urwid_ui import ManifestBrowser
44from checkbox_ng.urwid_ui import ReRunBrowser46from checkbox_ng.urwid_ui import ReRunBrowser
45from checkbox_ng.urwid_ui import interrupt_dialog47from checkbox_ng.urwid_ui import interrupt_dialog
46from checkbox_ng.urwid_ui import resume_dialog48from checkbox_ng.urwid_ui import resume_dialog
@@ -284,7 +286,13 @@ class RemoteMaster(ReportsStage, MainLoopStage):
284 self.jobs = self.sa.finish_bootstrap()286 self.jobs = self.sa.finish_bootstrap()
285287
286 def select_jobs(self, all_jobs):288 def select_jobs(self, all_jobs):
287 if not self.launcher.test_selection_forced:289 if self.launcher.test_selection_forced:
290 if self.launcher.manifest is not Unset:
291 self.sa.save_manifest(
292 {manifest_id: self.launcher.manifest[manifest_id] for
293 manifest_id in self.launcher.manifest}
294 )
295 else:
288 _logger.info("master: Selecting jobs.")296 _logger.info("master: Selecting jobs.")
289 reprs = self.sa.get_jobs_repr(all_jobs)297 reprs = self.sa.get_jobs_repr(all_jobs)
290 wanted_set = CategoryBrowser(298 wanted_set = CategoryBrowser(
@@ -296,6 +304,11 @@ class RemoteMaster(ReportsStage, MainLoopStage):
296 chosen_jobs = [job for job in all_jobs if job in wanted_set]304 chosen_jobs = [job for job in all_jobs if job in wanted_set]
297 _logger.debug("master: Selected jobs: %s", chosen_jobs)305 _logger.debug("master: Selected jobs: %s", chosen_jobs)
298 self.sa.modify_todo_list(chosen_jobs)306 self.sa.modify_todo_list(chosen_jobs)
307 manifest_repr = self.sa.get_manifest_repr()
308 if manifest_repr:
309 manifest_answers = ManifestBrowser(
310 "System Manifest:", manifest_repr).run()
311 self.sa.save_manifest(manifest_answers)
299 self.sa.finish_job_selection()312 self.sa.finish_job_selection()
300 self.run_jobs()313 self.run_jobs()
301314
diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
index 2a33b13..75e6770 100644
--- a/checkbox_ng/launcher/subcommands.py
+++ b/checkbox_ng/launcher/subcommands.py
@@ -47,6 +47,7 @@ from plainbox.impl.providers import get_providers
47from plainbox.impl.providers.embedded_providers import (47from plainbox.impl.providers.embedded_providers import (
48 EmbeddedProvider1PlugInCollection)48 EmbeddedProvider1PlugInCollection)
49from plainbox.impl.result import MemoryJobResult49from plainbox.impl.result import MemoryJobResult
50from plainbox.impl.secure.config import Unset
50from plainbox.impl.secure.sudo_broker import sudo_password_provider51from plainbox.impl.secure.sudo_broker import sudo_password_provider
51from plainbox.impl.session.assistant import SessionAssistant, SA_RESTARTABLE52from plainbox.impl.session.assistant import SessionAssistant, SA_RESTARTABLE
52from plainbox.impl.session.jobs import InhibitionCause53from plainbox.impl.session.jobs import InhibitionCause
@@ -64,6 +65,7 @@ from checkbox_ng.launcher.startprovider import (
64from checkbox_ng.launcher.run import Action65from checkbox_ng.launcher.run import Action
65from checkbox_ng.launcher.run import NormalUI66from checkbox_ng.launcher.run import NormalUI
66from checkbox_ng.urwid_ui import CategoryBrowser67from checkbox_ng.urwid_ui import CategoryBrowser
68from checkbox_ng.urwid_ui import ManifestBrowser
67from checkbox_ng.urwid_ui import ReRunBrowser69from checkbox_ng.urwid_ui import ReRunBrowser
68from checkbox_ng.urwid_ui import TestPlanBrowser70from checkbox_ng.urwid_ui import TestPlanBrowser
6971
@@ -401,6 +403,11 @@ class Launcher(MainLoopStage, ReportsStage):
401403
402 def _pick_jobs_to_run(self):404 def _pick_jobs_to_run(self):
403 if self.launcher.test_selection_forced:405 if self.launcher.test_selection_forced:
406 if self.launcher.manifest is not Unset:
407 self.ctx.sa.save_manifest(
408 {manifest_id: self.launcher.manifest[manifest_id] for
409 manifest_id in self.launcher.manifest}
410 )
404 # by default all tests are selected; so we're done here411 # by default all tests are selected; so we're done here
405 return412 return
406 job_list = [self.ctx.sa.get_job(job_id) for job_id in413 job_list = [self.ctx.sa.get_job(job_id) for job_id in
@@ -411,6 +418,11 @@ class Launcher(MainLoopStage, ReportsStage):
411 test_info_list = self._generate_job_infos(job_list)418 test_info_list = self._generate_job_infos(job_list)
412 wanted_set = CategoryBrowser(419 wanted_set = CategoryBrowser(
413 _("Choose tests to run on your system:"), test_info_list).run()420 _("Choose tests to run on your system:"), test_info_list).run()
421 manifest_repr = self.ctx.sa.get_manifest_repr()
422 if manifest_repr:
423 manifest_answers = ManifestBrowser(
424 "System Manifest:", manifest_repr).run()
425 self.ctx.sa.save_manifest(manifest_answers)
414 # no need to set an alternate selection if the job list not changed426 # no need to set an alternate selection if the job list not changed
415 if len(test_info_list) == len(wanted_set):427 if len(test_info_list) == len(wanted_set):
416 return428 return
diff --git a/checkbox_ng/urwid_ui.py b/checkbox_ng/urwid_ui.py
index 569e0df..2d5b579 100644
--- a/checkbox_ng/urwid_ui.py
+++ b/checkbox_ng/urwid_ui.py
@@ -744,6 +744,137 @@ class CountdownWidget(urwid.BigText):
744 raise urwid.ExitMainLoop744 raise urwid.ExitMainLoop
745745
746746
747class ManifestNaturalEdit(urwid.IntEdit):
748
749 def keypress(self, size, key):
750 (maxcol,) = size
751 return urwid.Edit.keypress(self, (maxcol,), key)
752
753 def value(self):
754 if self.edit_text:
755 return int(self.edit_text)
756
757
758class ManifestQuestion(urwid.WidgetWrap):
759
760 def __init__(self, question):
761 self.id = question['id']
762 self._value = question['value']
763 self._value_type = question['value_type']
764 if self._value_type == 'bool':
765 self.options = []
766 yes = urwid.RadioButton(
767 self.options, "Yes", state=False,
768 on_state_change=self._set_bool_value)
769 no = urwid.RadioButton(
770 self.options, "No", state=False,
771 on_state_change=self._set_bool_value)
772 if question['value'] is not None:
773 if question['value'] is True:
774 yes.set_state(True)
775 else:
776 no.set_state(True)
777 self.display_widget = urwid.Columns([
778 urwid.Padding(urwid.Text(question['name']), left=2),
779 urwid.GridFlow([yes, no], 7, 3, 1, align='left')
780 ], dividechars=5)
781 urwid.WidgetWrap.__init__(self, self.display_widget)
782 elif self._value_type == 'natural':
783 self._edit_widget = ManifestNaturalEdit(u"", self._value)
784 self.display_widget = urwid.Columns([
785 urwid.Padding(urwid.Text(question['name']), left=2),
786 (8, urwid.Padding(urwid.Text("["), left=7)),
787 self._edit_widget,
788 (1, urwid.Text("]"))
789 ])
790 urwid.WidgetWrap.__init__(self, self.display_widget)
791
792 def _set_bool_value(self, w, new_state, user_data=None):
793 if w.label == 'Yes' and new_state:
794 self._value = new_state
795 elif w.label == 'No' and new_state:
796 self._value = False
797
798 @property
799 def value(self):
800 if self._value_type == 'bool':
801 return self._value
802 elif self._value_type == 'natural':
803 return self._edit_widget.value()
804
805
806class ManifestBrowser:
807 palette = [
808 ('body', 'light gray', 'black'),
809 ('buttnf', 'black', 'light gray'),
810 ('buttn', 'light gray', 'black', 'bold'),
811 ('head', 'black', 'light gray', 'standout'),
812 ('foot', 'light gray', 'black'),
813 ('title', 'white', 'black', 'bold'),
814 ('start', 'dark green,bold', 'black'),
815 ('bold', 'bold', 'black'),
816 ]
817
818 footer_text = [('Press ('), ('start', 'T'), (') to start Testing')]
819 footer_shortcuts = [('Shortcuts: '), ('bold', 'y'), ('/'), ('bold', 'n ')]
820
821 def __init__(self, title, manifest):
822 self.manifest = manifest
823 self._manifest_out = {}
824 self._widget_cache = []
825 # Header
826 self.header = urwid.Padding(urwid.Text(title), left=1)
827 # Body
828 content = []
829 for prompt, questions in sorted(self.manifest.items()):
830 content.append(urwid.Text(prompt))
831 for q in sorted(questions, key=lambda i: i['name']):
832 question_widget = ManifestQuestion(q)
833 content.append(urwid.AttrWrap(question_widget,
834 'buttn', 'buttnf'))
835 self._widget_cache.append(question_widget)
836 self._pile = urwid.Pile(content)
837 listbox_content = [
838 urwid.Padding(self._pile, left=1, right=1, min_width=13),
839 ]
840 self.listbox = urwid.ListBox(urwid.SimpleListWalker(listbox_content))
841 # Footer
842 self.default_footer = urwid.AttrWrap(urwid.Columns(
843 [urwid.Padding(urwid.Text(self.footer_text), left=1),
844 urwid.Text(self.footer_shortcuts, 'right')]), 'foot')
845 # Main frame
846 self.frame = urwid.Frame(
847 urwid.AttrWrap(urwid.LineBox(self.listbox), 'body'),
848 header=urwid.AttrWrap(self.header, 'head'),
849 footer=self.default_footer)
850
851 def run(self):
852 """Run the urwid MainLoop."""
853 self.loop = urwid.MainLoop(
854 self.frame, self.palette, unhandled_input=self.unhandled_input,
855 handle_mouse=False)
856 self.loop.run()
857 for w in self._widget_cache:
858 self._manifest_out.update({w.id: w.value})
859 return self._manifest_out
860
861 def unhandled_input(self, key):
862 if key in ('t', 'T'):
863 for w in self._widget_cache:
864 if w.value is None:
865 break
866 else:
867 raise urwid.ExitMainLoop()
868 if self._pile.focus._value_type == 'bool':
869 if key in ('y', 'Y'):
870 self.loop.process_input(["left", " ", "down"])
871 elif key in ('n', 'N'):
872 self.loop.process_input(["right", " ", "down"])
873 elif self._pile.focus._value_type == 'natural':
874 if key == 'enter':
875 self.loop.process_input(["down"])
876
877
747def resume_dialog(duration):878def resume_dialog(duration):
748 palette = [879 palette = [
749 ('body', 'light gray', 'black', 'standout'),880 ('body', 'light gray', 'black', 'standout'),
diff --git a/docs/launcher-tutorial.rst b/docs/launcher-tutorial.rst
index 81c8d41..5d50e48 100644
--- a/docs/launcher-tutorial.rst
+++ b/docs/launcher-tutorial.rst
@@ -392,6 +392,26 @@ Checkbox-slave daemon is run by root so in order to run some jobs as an
392unpriviledged user this variable can be used.392unpriviledged user this variable can be used.
393393
394394
395Manifest section
396================
397
398``[manifest]``
399
400Beginning of the manifest section.
401
402Each variable present in the ``manifest`` section will be used a a preset value
403for the system manifest, taking precedence over the disk cache.
404
405Example:
406
407::
408
409 [manifest]
410 com.canonical.certification::has_touchscreen = yes
411 com.canonical.certification::has_usb_type_c = true
412 com.canonical.certification::foo = 23
413
414
395Generating reports415Generating reports
396==================416==================
397417
diff --git a/plainbox/impl/launcher.py b/plainbox/impl/launcher.py
index 9c0b8f6..713d0af 100644
--- a/plainbox/impl/launcher.py
+++ b/plainbox/impl/launcher.py
@@ -252,5 +252,7 @@ class LauncherDefinition1(LauncherDefinition):
252 name='daemon',252 name='daemon',
253 help_text=_('Daemon-specific configuration'))253 help_text=_('Daemon-specific configuration'))
254254
255 manifest = config.Section(
256 help_text=_('Manifest entries to use'))
255257
256DefaultLauncherDefinition = LauncherDefinition1258DefaultLauncherDefinition = LauncherDefinition1
diff --git a/plainbox/impl/resource.py b/plainbox/impl/resource.py
index 26dacc7..a0a74a6 100644
--- a/plainbox/impl/resource.py
+++ b/plainbox/impl/resource.py
@@ -394,6 +394,7 @@ class ResourceNodeVisitor(ast.NodeVisitor):
394 """394 """
395 self._ids_seen_set = set()395 self._ids_seen_set = set()
396 self._ids_seen_list = []396 self._ids_seen_list = []
397 self._manifest_attr_seen_list = []
397398
398 @property399 @property
399 def ids_seen_set(self):400 def ids_seen_set(self):
@@ -409,6 +410,13 @@ class ResourceNodeVisitor(ast.NodeVisitor):
409 """410 """
410 return self._ids_seen_list411 return self._ids_seen_list
411412
413 @property
414 def manifest_attr_seen_list(self):
415 """
416 list() of ast.Attribute().attr values seen
417 """
418 return self._manifest_attr_seen_list
419
412 def visit_Name(self, node):420 def visit_Name(self, node):
413 """421 """
414 Internal method of NodeVisitor.422 Internal method of NodeVisitor.
@@ -421,6 +429,20 @@ class ResourceNodeVisitor(ast.NodeVisitor):
421 self._ids_seen_set.add(node.id)429 self._ids_seen_set.add(node.id)
422 self._ids_seen_list.append(node.id)430 self._ids_seen_list.append(node.id)
423431
432 def visit_Attribute(self, node):
433 """
434 Internal method of NodeVisitor.
435
436 This method is called whenever generic_visit() looks at an instance of
437 ast.Attribute(). It records the attr identifier
438 """
439 self._check_node(node)
440 if isinstance(node.value, ast.Name):
441 self.visit_Name(node.value)
442 if node.value.id == 'manifest':
443 if node.attr not in self._manifest_attr_seen_list:
444 self._manifest_attr_seen_list.append(node.attr)
445
424 def visit_Call(self, node):446 def visit_Call(self, node):
425 """447 """
426 Internal method of NodeVisitor.448 Internal method of NodeVisitor.
@@ -516,6 +538,7 @@ class ResourceExpression:
516 """538 """
517 self._implicit_namespace = implicit_namespace539 self._implicit_namespace = implicit_namespace
518 self._resource_alias_list = self._analyze(text)540 self._resource_alias_list = self._analyze(text)
541 self._manifest_id_list = self._analyze_manifest(text)
519 self._resource_id_list = []542 self._resource_id_list = []
520 if imports is None:543 if imports is None:
521 imports = ()544 imports = ()
@@ -572,6 +595,10 @@ class ResourceExpression:
572 ]595 ]
573596
574 @property597 @property
598 def manifest_id_list(self):
599 return self._manifest_id_list
600
601 @property
575 def resource_alias_list(self):602 def resource_alias_list(self):
576 """603 """
577 The alias of the resource object this expression operates on604 The alias of the resource object this expression operates on
@@ -648,6 +675,33 @@ class ResourceExpression:
648 else:675 else:
649 return list(visitor.ids_seen_list)676 return list(visitor.ids_seen_list)
650677
678 def _analyze_manifest(self, text):
679 """
680 Analyze the expression and return the id of the manifest resource
681
682 May raise SyntaxError or a ResourceProgramError subclass
683 """
684 # Use the ast module to build an abstract syntax tree of the expression
685 try:
686 node = ast.parse(text)
687 except SyntaxError:
688 raise ResourceSyntaxError
689 # Use ResourceNodeVisitor to see what kind of ast.Name objects are
690 # referenced by the expression. This may also raise CodeNotAllowed
691 # which should be captured by the higher layers.
692 visitor = ResourceNodeVisitor()
693 visitor.visit(node)
694 # Bail if the expression is not using exactly one resource id
695 if len(visitor.ids_seen_list) == 0:
696 raise NoResourcesReferenced()
697 else:
698 return [
699 "{}::{}".format(self._implicit_namespace, manifest_id)
700 if "::" not in manifest_id and self._implicit_namespace
701 else manifest_id
702 for manifest_id in list(visitor.manifest_attr_seen_list)
703 ]
704
651705
652def parse_imports_stmt(imports):706def parse_imports_stmt(imports):
653 """707 """
diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
index 8ef05e7..653bc4c 100644
--- a/plainbox/impl/session/assistant.py
+++ b/plainbox/impl/session/assistant.py
@@ -50,9 +50,11 @@ from plainbox.impl.providers.embedded_providers import (
50from plainbox.impl.result import JobResultBuilder50from plainbox.impl.result import JobResultBuilder
51from plainbox.impl.result import MemoryJobResult51from plainbox.impl.result import MemoryJobResult
52from plainbox.impl.runner import JobRunnerUIDelegate52from plainbox.impl.runner import JobRunnerUIDelegate
53from plainbox.impl.secure.config import Unset
53from plainbox.impl.secure.origin import Origin54from plainbox.impl.secure.origin import Origin
54from plainbox.impl.secure.qualifiers import select_jobs55from plainbox.impl.secure.qualifiers import select_jobs
55from plainbox.impl.secure.qualifiers import FieldQualifier56from plainbox.impl.secure.qualifiers import FieldQualifier
57from plainbox.impl.secure.qualifiers import JobIdQualifier
56from plainbox.impl.secure.qualifiers import PatternMatcher58from plainbox.impl.secure.qualifiers import PatternMatcher
57from plainbox.impl.secure.qualifiers import RegExpJobQualifier59from plainbox.impl.secure.qualifiers import RegExpJobQualifier
58from plainbox.impl.session import SessionMetaData60from plainbox.impl.session import SessionMetaData
@@ -209,6 +211,9 @@ class SessionAssistant:
209 "configure automatic restart capability")211 "configure automatic restart capability")
210 allowed_calls[self.use_alternate_restart_strategy] = (212 allowed_calls[self.use_alternate_restart_strategy] = (
211 "configure automatic restart capability")213 "configure automatic restart capability")
214 # Manifest
215 self._manifest_path = os.path.expanduser(
216 '~/.local/share/plainbox/machine-manifest.json')
212217
213 @raises(UnexpectedMethodCall, LookupError)218 @raises(UnexpectedMethodCall, LookupError)
214 def configure_application_restart(219 def configure_application_restart(
@@ -930,7 +935,9 @@ class SessionAssistant:
930 desired_job_list = select_jobs(935 desired_job_list = select_jobs(
931 self._context.state.job_list,936 self._context.state.job_list,
932 [plan.get_qualifier() for plan in self._manager.test_plans] +937 [plan.get_qualifier() for plan in self._manager.test_plans] +
933 self._exclude_qualifiers)938 self._exclude_qualifiers +
939 [JobIdQualifier(
940 'com.canonical.plainbox::collect-manifest', None, False)])
934 self._context.state.update_desired_job_list(desired_job_list)941 self._context.state.update_desired_job_list(desired_job_list)
935 # Set subsequent usage expectations i.e. all of the runtime parts are942 # Set subsequent usage expectations i.e. all of the runtime parts are
936 # available now.943 # available now.
@@ -1244,6 +1251,100 @@ class SessionAssistant:
1244 if jsm[job.id].result.outcome is None1251 if jsm[job.id].result.outcome is None
1245 ]1252 ]
12461253
1254 def _strtobool(self, val):
1255 return val.lower() in ('y', 'yes', 't', 'true', 'on', '1')
1256
1257 @raises(SystemExit, UnexpectedMethodCall)
1258 def get_manifest_repr(self) -> 'Dict[List[Dict]]':
1259 """
1260 Get the manifest units required by the jobs selection.
1261
1262 :returns:
1263 A dict of manifest questions.
1264 :raises SystemExit:
1265 When the launcher manifest section contains invalid entries.
1266 :raises UnexpectedMethodCall:
1267 If the call is made at an unexpected time. Do not catch this error.
1268 It is a bug in your program. The error message will indicate what
1269 is the likely cause.
1270 """
1271 UsageExpectation.of(self).enforce()
1272 # XXX: job_state_map is a bit low level, can we avoid that?
1273 jsm = self._context.state.job_state_map
1274 todo_list = [
1275 job for job in self._context.state.run_list
1276 if jsm[job.id].result.outcome is None
1277 ]
1278 expression_list = []
1279 manifest_id_set = set()
1280 for job in todo_list:
1281 if job.get_resource_program():
1282 expression_list.extend(
1283 job.get_resource_program().expression_list)
1284 for e in expression_list:
1285 manifest_id_set.update(e.manifest_id_list)
1286 manifest_list = [unit for unit in self._context.unit_list
1287 if unit.Meta.name == 'manifest entry'
1288 and unit.id in manifest_id_set]
1289 manifest_cache = {}
1290 if os.path.isfile(self._manifest_path):
1291 with open(self._manifest_path, 'rt', encoding='UTF-8') as stream:
1292 manifest_cache = json.load(stream)
1293 if self._config is not None and self._config.manifest is not Unset:
1294 for manifest_id in self._config.manifest:
1295 manifest_cache.update(
1296 {manifest_id: self._config.manifest[manifest_id]})
1297 manifest_info_dict = dict()
1298 for m in manifest_list:
1299 prompt = m.prompt()
1300 if prompt is None:
1301 if m.value_type == 'bool':
1302 prompt = "Does this machine have this piece of hardware?"
1303 elif m.value_type == 'natural':
1304 prompt = "Please enter the requested data:"
1305 else:
1306 _logger.error("Unsupported value-type: '%s'", m.value_type)
1307 continue
1308 if prompt not in manifest_info_dict:
1309 manifest_info_dict[prompt] = []
1310 manifest_info = {
1311 "id": m.id,
1312 "partial_id": m.partial_id,
1313 "name": m.name,
1314 "value_type": m.value_type,
1315 }
1316 try:
1317 value = manifest_cache[m.id]
1318 if m.value_type == 'bool':
1319 if isinstance(manifest_cache[m.id], str):
1320 value = self._strtobool(manifest_cache[m.id])
1321 elif m.value_type == 'natural':
1322 value = int(manifest_cache[m.id])
1323 except ValueError:
1324 _logger.error(
1325 ("Invalid manifest %s value '%s'"),
1326 m.id, manifest_cache[m.id])
1327 raise SystemExit(1)
1328 except KeyError:
1329 value = None
1330 manifest_info.update({'value': value})
1331 manifest_info_dict[prompt].append(manifest_info)
1332 return manifest_info_dict
1333
1334 def save_manifest(self, manifest_answers):
1335 """
1336 Record the manifest on disk.
1337 """
1338 manifest_cache = dict()
1339 if os.path.isfile(self._manifest_path):
1340 with open(self._manifest_path, 'rt', encoding='UTF-8') as stream:
1341 manifest_cache = json.load(stream)
1342 os.makedirs(os.path.dirname(self._manifest_path), exist_ok=True)
1343 manifest_cache.update(manifest_answers)
1344 print("Saving manifest to {}".format(self._manifest_path))
1345 with open(self._manifest_path, 'wt', encoding='UTF-8') as stream:
1346 json.dump(manifest_cache, stream, sort_keys=True, indent=2)
1347
1247 @raises(ValueError, TypeError, UnexpectedMethodCall)1348 @raises(ValueError, TypeError, UnexpectedMethodCall)
1248 def run_job(1349 def run_job(
1249 self, job_id: str, ui: 'Union[str, IJobRunnerUI]',1350 self, job_id: str, ui: 'Union[str, IJobRunnerUI]',
@@ -1728,6 +1829,8 @@ class SessionAssistant:
1728 self.remove_all_filters: "to remove all filters",1829 self.remove_all_filters: "to remove all filters",
1729 self.get_static_todo_list: "to see what is meant to be executed",1830 self.get_static_todo_list: "to see what is meant to be executed",
1730 self.get_dynamic_todo_list: "to see what is yet to be executed",1831 self.get_dynamic_todo_list: "to see what is yet to be executed",
1832 self.get_manifest_repr: (
1833 "to get participating manifest units"),
1731 self.run_job: "to run a given job",1834 self.run_job: "to run a given job",
1732 self.use_alternate_selection: "to change the selection",1835 self.use_alternate_selection: "to change the selection",
1733 self.hand_pick_jobs: "to generate new selection and use it",1836 self.hand_pick_jobs: "to generate new selection and use it",
diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
index 9b91aeb..f5eb00e 100644
--- a/plainbox/impl/session/remote_assistant.py
+++ b/plainbox/impl/session/remote_assistant.py
@@ -286,6 +286,12 @@ class RemoteSessionAssistant():
286 job_state.attempts = self._launcher.max_attempts286 job_state.attempts = self._launcher.max_attempts
287 return self._sa.get_static_todo_list()287 return self._sa.get_static_todo_list()
288288
289 def get_manifest_repr(self):
290 return self._sa.get_manifest_repr()
291
292 def save_manifest(self, manifest_answers):
293 return self._sa.save_manifest(manifest_answers)
294
289 def modify_todo_list(self, chosen_jobs):295 def modify_todo_list(self, chosen_jobs):
290 self._sa.use_alternate_selection(chosen_jobs)296 self._sa.use_alternate_selection(chosen_jobs)
291297

Subscribers

People subscribed via source and target branches