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 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
1diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py
2index 5c9afcf..2107fbf 100644
3--- a/checkbox_ng/launcher/master.py
4+++ b/checkbox_ng/launcher/master.py
5@@ -36,11 +36,13 @@ from tempfile import SpooledTemporaryFile
6
7 from plainbox.impl.color import Colorizer
8 from plainbox.impl.launcher import DefaultLauncherDefinition
9+from plainbox.impl.secure.config import Unset
10 from plainbox.impl.secure.sudo_broker import SudoProvider
11 from plainbox.impl.session.remote_assistant import RemoteSessionAssistant
12 from plainbox.vendor import rpyc
13 from checkbox_ng.urwid_ui import TestPlanBrowser
14 from checkbox_ng.urwid_ui import CategoryBrowser
15+from checkbox_ng.urwid_ui import ManifestBrowser
16 from checkbox_ng.urwid_ui import ReRunBrowser
17 from checkbox_ng.urwid_ui import interrupt_dialog
18 from checkbox_ng.urwid_ui import resume_dialog
19@@ -284,7 +286,13 @@ class RemoteMaster(ReportsStage, MainLoopStage):
20 self.jobs = self.sa.finish_bootstrap()
21
22 def select_jobs(self, all_jobs):
23- if not self.launcher.test_selection_forced:
24+ if self.launcher.test_selection_forced:
25+ if self.launcher.manifest is not Unset:
26+ self.sa.save_manifest(
27+ {manifest_id: self.launcher.manifest[manifest_id] for
28+ manifest_id in self.launcher.manifest}
29+ )
30+ else:
31 _logger.info("master: Selecting jobs.")
32 reprs = self.sa.get_jobs_repr(all_jobs)
33 wanted_set = CategoryBrowser(
34@@ -296,6 +304,11 @@ class RemoteMaster(ReportsStage, MainLoopStage):
35 chosen_jobs = [job for job in all_jobs if job in wanted_set]
36 _logger.debug("master: Selected jobs: %s", chosen_jobs)
37 self.sa.modify_todo_list(chosen_jobs)
38+ manifest_repr = self.sa.get_manifest_repr()
39+ if manifest_repr:
40+ manifest_answers = ManifestBrowser(
41+ "System Manifest:", manifest_repr).run()
42+ self.sa.save_manifest(manifest_answers)
43 self.sa.finish_job_selection()
44 self.run_jobs()
45
46diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
47index 2a33b13..75e6770 100644
48--- a/checkbox_ng/launcher/subcommands.py
49+++ b/checkbox_ng/launcher/subcommands.py
50@@ -47,6 +47,7 @@ from plainbox.impl.providers import get_providers
51 from plainbox.impl.providers.embedded_providers import (
52 EmbeddedProvider1PlugInCollection)
53 from plainbox.impl.result import MemoryJobResult
54+from plainbox.impl.secure.config import Unset
55 from plainbox.impl.secure.sudo_broker import sudo_password_provider
56 from plainbox.impl.session.assistant import SessionAssistant, SA_RESTARTABLE
57 from plainbox.impl.session.jobs import InhibitionCause
58@@ -64,6 +65,7 @@ from checkbox_ng.launcher.startprovider import (
59 from checkbox_ng.launcher.run import Action
60 from checkbox_ng.launcher.run import NormalUI
61 from checkbox_ng.urwid_ui import CategoryBrowser
62+from checkbox_ng.urwid_ui import ManifestBrowser
63 from checkbox_ng.urwid_ui import ReRunBrowser
64 from checkbox_ng.urwid_ui import TestPlanBrowser
65
66@@ -401,6 +403,11 @@ class Launcher(MainLoopStage, ReportsStage):
67
68 def _pick_jobs_to_run(self):
69 if self.launcher.test_selection_forced:
70+ if self.launcher.manifest is not Unset:
71+ self.ctx.sa.save_manifest(
72+ {manifest_id: self.launcher.manifest[manifest_id] for
73+ manifest_id in self.launcher.manifest}
74+ )
75 # by default all tests are selected; so we're done here
76 return
77 job_list = [self.ctx.sa.get_job(job_id) for job_id in
78@@ -411,6 +418,11 @@ class Launcher(MainLoopStage, ReportsStage):
79 test_info_list = self._generate_job_infos(job_list)
80 wanted_set = CategoryBrowser(
81 _("Choose tests to run on your system:"), test_info_list).run()
82+ manifest_repr = self.ctx.sa.get_manifest_repr()
83+ if manifest_repr:
84+ manifest_answers = ManifestBrowser(
85+ "System Manifest:", manifest_repr).run()
86+ self.ctx.sa.save_manifest(manifest_answers)
87 # no need to set an alternate selection if the job list not changed
88 if len(test_info_list) == len(wanted_set):
89 return
90diff --git a/checkbox_ng/urwid_ui.py b/checkbox_ng/urwid_ui.py
91index 569e0df..2d5b579 100644
92--- a/checkbox_ng/urwid_ui.py
93+++ b/checkbox_ng/urwid_ui.py
94@@ -744,6 +744,137 @@ class CountdownWidget(urwid.BigText):
95 raise urwid.ExitMainLoop
96
97
98+class ManifestNaturalEdit(urwid.IntEdit):
99+
100+ def keypress(self, size, key):
101+ (maxcol,) = size
102+ return urwid.Edit.keypress(self, (maxcol,), key)
103+
104+ def value(self):
105+ if self.edit_text:
106+ return int(self.edit_text)
107+
108+
109+class ManifestQuestion(urwid.WidgetWrap):
110+
111+ def __init__(self, question):
112+ self.id = question['id']
113+ self._value = question['value']
114+ self._value_type = question['value_type']
115+ if self._value_type == 'bool':
116+ self.options = []
117+ yes = urwid.RadioButton(
118+ self.options, "Yes", state=False,
119+ on_state_change=self._set_bool_value)
120+ no = urwid.RadioButton(
121+ self.options, "No", state=False,
122+ on_state_change=self._set_bool_value)
123+ if question['value'] is not None:
124+ if question['value'] is True:
125+ yes.set_state(True)
126+ else:
127+ no.set_state(True)
128+ self.display_widget = urwid.Columns([
129+ urwid.Padding(urwid.Text(question['name']), left=2),
130+ urwid.GridFlow([yes, no], 7, 3, 1, align='left')
131+ ], dividechars=5)
132+ urwid.WidgetWrap.__init__(self, self.display_widget)
133+ elif self._value_type == 'natural':
134+ self._edit_widget = ManifestNaturalEdit(u"", self._value)
135+ self.display_widget = urwid.Columns([
136+ urwid.Padding(urwid.Text(question['name']), left=2),
137+ (8, urwid.Padding(urwid.Text("["), left=7)),
138+ self._edit_widget,
139+ (1, urwid.Text("]"))
140+ ])
141+ urwid.WidgetWrap.__init__(self, self.display_widget)
142+
143+ def _set_bool_value(self, w, new_state, user_data=None):
144+ if w.label == 'Yes' and new_state:
145+ self._value = new_state
146+ elif w.label == 'No' and new_state:
147+ self._value = False
148+
149+ @property
150+ def value(self):
151+ if self._value_type == 'bool':
152+ return self._value
153+ elif self._value_type == 'natural':
154+ return self._edit_widget.value()
155+
156+
157+class ManifestBrowser:
158+ palette = [
159+ ('body', 'light gray', 'black'),
160+ ('buttnf', 'black', 'light gray'),
161+ ('buttn', 'light gray', 'black', 'bold'),
162+ ('head', 'black', 'light gray', 'standout'),
163+ ('foot', 'light gray', 'black'),
164+ ('title', 'white', 'black', 'bold'),
165+ ('start', 'dark green,bold', 'black'),
166+ ('bold', 'bold', 'black'),
167+ ]
168+
169+ footer_text = [('Press ('), ('start', 'T'), (') to start Testing')]
170+ footer_shortcuts = [('Shortcuts: '), ('bold', 'y'), ('/'), ('bold', 'n ')]
171+
172+ def __init__(self, title, manifest):
173+ self.manifest = manifest
174+ self._manifest_out = {}
175+ self._widget_cache = []
176+ # Header
177+ self.header = urwid.Padding(urwid.Text(title), left=1)
178+ # Body
179+ content = []
180+ for prompt, questions in sorted(self.manifest.items()):
181+ content.append(urwid.Text(prompt))
182+ for q in sorted(questions, key=lambda i: i['name']):
183+ question_widget = ManifestQuestion(q)
184+ content.append(urwid.AttrWrap(question_widget,
185+ 'buttn', 'buttnf'))
186+ self._widget_cache.append(question_widget)
187+ self._pile = urwid.Pile(content)
188+ listbox_content = [
189+ urwid.Padding(self._pile, left=1, right=1, min_width=13),
190+ ]
191+ self.listbox = urwid.ListBox(urwid.SimpleListWalker(listbox_content))
192+ # Footer
193+ self.default_footer = urwid.AttrWrap(urwid.Columns(
194+ [urwid.Padding(urwid.Text(self.footer_text), left=1),
195+ urwid.Text(self.footer_shortcuts, 'right')]), 'foot')
196+ # Main frame
197+ self.frame = urwid.Frame(
198+ urwid.AttrWrap(urwid.LineBox(self.listbox), 'body'),
199+ header=urwid.AttrWrap(self.header, 'head'),
200+ footer=self.default_footer)
201+
202+ def run(self):
203+ """Run the urwid MainLoop."""
204+ self.loop = urwid.MainLoop(
205+ self.frame, self.palette, unhandled_input=self.unhandled_input,
206+ handle_mouse=False)
207+ self.loop.run()
208+ for w in self._widget_cache:
209+ self._manifest_out.update({w.id: w.value})
210+ return self._manifest_out
211+
212+ def unhandled_input(self, key):
213+ if key in ('t', 'T'):
214+ for w in self._widget_cache:
215+ if w.value is None:
216+ break
217+ else:
218+ raise urwid.ExitMainLoop()
219+ if self._pile.focus._value_type == 'bool':
220+ if key in ('y', 'Y'):
221+ self.loop.process_input(["left", " ", "down"])
222+ elif key in ('n', 'N'):
223+ self.loop.process_input(["right", " ", "down"])
224+ elif self._pile.focus._value_type == 'natural':
225+ if key == 'enter':
226+ self.loop.process_input(["down"])
227+
228+
229 def resume_dialog(duration):
230 palette = [
231 ('body', 'light gray', 'black', 'standout'),
232diff --git a/docs/launcher-tutorial.rst b/docs/launcher-tutorial.rst
233index 81c8d41..5d50e48 100644
234--- a/docs/launcher-tutorial.rst
235+++ b/docs/launcher-tutorial.rst
236@@ -392,6 +392,26 @@ Checkbox-slave daemon is run by root so in order to run some jobs as an
237 unpriviledged user this variable can be used.
238
239
240+Manifest section
241+================
242+
243+``[manifest]``
244+
245+Beginning of the manifest section.
246+
247+Each variable present in the ``manifest`` section will be used a a preset value
248+for the system manifest, taking precedence over the disk cache.
249+
250+Example:
251+
252+::
253+
254+ [manifest]
255+ com.canonical.certification::has_touchscreen = yes
256+ com.canonical.certification::has_usb_type_c = true
257+ com.canonical.certification::foo = 23
258+
259+
260 Generating reports
261 ==================
262
263diff --git a/plainbox/impl/launcher.py b/plainbox/impl/launcher.py
264index 9c0b8f6..713d0af 100644
265--- a/plainbox/impl/launcher.py
266+++ b/plainbox/impl/launcher.py
267@@ -252,5 +252,7 @@ class LauncherDefinition1(LauncherDefinition):
268 name='daemon',
269 help_text=_('Daemon-specific configuration'))
270
271+ manifest = config.Section(
272+ help_text=_('Manifest entries to use'))
273
274 DefaultLauncherDefinition = LauncherDefinition1
275diff --git a/plainbox/impl/resource.py b/plainbox/impl/resource.py
276index 26dacc7..a0a74a6 100644
277--- a/plainbox/impl/resource.py
278+++ b/plainbox/impl/resource.py
279@@ -394,6 +394,7 @@ class ResourceNodeVisitor(ast.NodeVisitor):
280 """
281 self._ids_seen_set = set()
282 self._ids_seen_list = []
283+ self._manifest_attr_seen_list = []
284
285 @property
286 def ids_seen_set(self):
287@@ -409,6 +410,13 @@ class ResourceNodeVisitor(ast.NodeVisitor):
288 """
289 return self._ids_seen_list
290
291+ @property
292+ def manifest_attr_seen_list(self):
293+ """
294+ list() of ast.Attribute().attr values seen
295+ """
296+ return self._manifest_attr_seen_list
297+
298 def visit_Name(self, node):
299 """
300 Internal method of NodeVisitor.
301@@ -421,6 +429,20 @@ class ResourceNodeVisitor(ast.NodeVisitor):
302 self._ids_seen_set.add(node.id)
303 self._ids_seen_list.append(node.id)
304
305+ def visit_Attribute(self, node):
306+ """
307+ Internal method of NodeVisitor.
308+
309+ This method is called whenever generic_visit() looks at an instance of
310+ ast.Attribute(). It records the attr identifier
311+ """
312+ self._check_node(node)
313+ if isinstance(node.value, ast.Name):
314+ self.visit_Name(node.value)
315+ if node.value.id == 'manifest':
316+ if node.attr not in self._manifest_attr_seen_list:
317+ self._manifest_attr_seen_list.append(node.attr)
318+
319 def visit_Call(self, node):
320 """
321 Internal method of NodeVisitor.
322@@ -516,6 +538,7 @@ class ResourceExpression:
323 """
324 self._implicit_namespace = implicit_namespace
325 self._resource_alias_list = self._analyze(text)
326+ self._manifest_id_list = self._analyze_manifest(text)
327 self._resource_id_list = []
328 if imports is None:
329 imports = ()
330@@ -572,6 +595,10 @@ class ResourceExpression:
331 ]
332
333 @property
334+ def manifest_id_list(self):
335+ return self._manifest_id_list
336+
337+ @property
338 def resource_alias_list(self):
339 """
340 The alias of the resource object this expression operates on
341@@ -648,6 +675,33 @@ class ResourceExpression:
342 else:
343 return list(visitor.ids_seen_list)
344
345+ def _analyze_manifest(self, text):
346+ """
347+ Analyze the expression and return the id of the manifest resource
348+
349+ May raise SyntaxError or a ResourceProgramError subclass
350+ """
351+ # Use the ast module to build an abstract syntax tree of the expression
352+ try:
353+ node = ast.parse(text)
354+ except SyntaxError:
355+ raise ResourceSyntaxError
356+ # Use ResourceNodeVisitor to see what kind of ast.Name objects are
357+ # referenced by the expression. This may also raise CodeNotAllowed
358+ # which should be captured by the higher layers.
359+ visitor = ResourceNodeVisitor()
360+ visitor.visit(node)
361+ # Bail if the expression is not using exactly one resource id
362+ if len(visitor.ids_seen_list) == 0:
363+ raise NoResourcesReferenced()
364+ else:
365+ return [
366+ "{}::{}".format(self._implicit_namespace, manifest_id)
367+ if "::" not in manifest_id and self._implicit_namespace
368+ else manifest_id
369+ for manifest_id in list(visitor.manifest_attr_seen_list)
370+ ]
371+
372
373 def parse_imports_stmt(imports):
374 """
375diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
376index 8ef05e7..653bc4c 100644
377--- a/plainbox/impl/session/assistant.py
378+++ b/plainbox/impl/session/assistant.py
379@@ -50,9 +50,11 @@ from plainbox.impl.providers.embedded_providers import (
380 from plainbox.impl.result import JobResultBuilder
381 from plainbox.impl.result import MemoryJobResult
382 from plainbox.impl.runner import JobRunnerUIDelegate
383+from plainbox.impl.secure.config import Unset
384 from plainbox.impl.secure.origin import Origin
385 from plainbox.impl.secure.qualifiers import select_jobs
386 from plainbox.impl.secure.qualifiers import FieldQualifier
387+from plainbox.impl.secure.qualifiers import JobIdQualifier
388 from plainbox.impl.secure.qualifiers import PatternMatcher
389 from plainbox.impl.secure.qualifiers import RegExpJobQualifier
390 from plainbox.impl.session import SessionMetaData
391@@ -209,6 +211,9 @@ class SessionAssistant:
392 "configure automatic restart capability")
393 allowed_calls[self.use_alternate_restart_strategy] = (
394 "configure automatic restart capability")
395+ # Manifest
396+ self._manifest_path = os.path.expanduser(
397+ '~/.local/share/plainbox/machine-manifest.json')
398
399 @raises(UnexpectedMethodCall, LookupError)
400 def configure_application_restart(
401@@ -930,7 +935,9 @@ class SessionAssistant:
402 desired_job_list = select_jobs(
403 self._context.state.job_list,
404 [plan.get_qualifier() for plan in self._manager.test_plans] +
405- self._exclude_qualifiers)
406+ self._exclude_qualifiers +
407+ [JobIdQualifier(
408+ 'com.canonical.plainbox::collect-manifest', None, False)])
409 self._context.state.update_desired_job_list(desired_job_list)
410 # Set subsequent usage expectations i.e. all of the runtime parts are
411 # available now.
412@@ -1244,6 +1251,100 @@ class SessionAssistant:
413 if jsm[job.id].result.outcome is None
414 ]
415
416+ def _strtobool(self, val):
417+ return val.lower() in ('y', 'yes', 't', 'true', 'on', '1')
418+
419+ @raises(SystemExit, UnexpectedMethodCall)
420+ def get_manifest_repr(self) -> 'Dict[List[Dict]]':
421+ """
422+ Get the manifest units required by the jobs selection.
423+
424+ :returns:
425+ A dict of manifest questions.
426+ :raises SystemExit:
427+ When the launcher manifest section contains invalid entries.
428+ :raises UnexpectedMethodCall:
429+ If the call is made at an unexpected time. Do not catch this error.
430+ It is a bug in your program. The error message will indicate what
431+ is the likely cause.
432+ """
433+ UsageExpectation.of(self).enforce()
434+ # XXX: job_state_map is a bit low level, can we avoid that?
435+ jsm = self._context.state.job_state_map
436+ todo_list = [
437+ job for job in self._context.state.run_list
438+ if jsm[job.id].result.outcome is None
439+ ]
440+ expression_list = []
441+ manifest_id_set = set()
442+ for job in todo_list:
443+ if job.get_resource_program():
444+ expression_list.extend(
445+ job.get_resource_program().expression_list)
446+ for e in expression_list:
447+ manifest_id_set.update(e.manifest_id_list)
448+ manifest_list = [unit for unit in self._context.unit_list
449+ if unit.Meta.name == 'manifest entry'
450+ and unit.id in manifest_id_set]
451+ manifest_cache = {}
452+ if os.path.isfile(self._manifest_path):
453+ with open(self._manifest_path, 'rt', encoding='UTF-8') as stream:
454+ manifest_cache = json.load(stream)
455+ if self._config is not None and self._config.manifest is not Unset:
456+ for manifest_id in self._config.manifest:
457+ manifest_cache.update(
458+ {manifest_id: self._config.manifest[manifest_id]})
459+ manifest_info_dict = dict()
460+ for m in manifest_list:
461+ prompt = m.prompt()
462+ if prompt is None:
463+ if m.value_type == 'bool':
464+ prompt = "Does this machine have this piece of hardware?"
465+ elif m.value_type == 'natural':
466+ prompt = "Please enter the requested data:"
467+ else:
468+ _logger.error("Unsupported value-type: '%s'", m.value_type)
469+ continue
470+ if prompt not in manifest_info_dict:
471+ manifest_info_dict[prompt] = []
472+ manifest_info = {
473+ "id": m.id,
474+ "partial_id": m.partial_id,
475+ "name": m.name,
476+ "value_type": m.value_type,
477+ }
478+ try:
479+ value = manifest_cache[m.id]
480+ if m.value_type == 'bool':
481+ if isinstance(manifest_cache[m.id], str):
482+ value = self._strtobool(manifest_cache[m.id])
483+ elif m.value_type == 'natural':
484+ value = int(manifest_cache[m.id])
485+ except ValueError:
486+ _logger.error(
487+ ("Invalid manifest %s value '%s'"),
488+ m.id, manifest_cache[m.id])
489+ raise SystemExit(1)
490+ except KeyError:
491+ value = None
492+ manifest_info.update({'value': value})
493+ manifest_info_dict[prompt].append(manifest_info)
494+ return manifest_info_dict
495+
496+ def save_manifest(self, manifest_answers):
497+ """
498+ Record the manifest on disk.
499+ """
500+ manifest_cache = dict()
501+ if os.path.isfile(self._manifest_path):
502+ with open(self._manifest_path, 'rt', encoding='UTF-8') as stream:
503+ manifest_cache = json.load(stream)
504+ os.makedirs(os.path.dirname(self._manifest_path), exist_ok=True)
505+ manifest_cache.update(manifest_answers)
506+ print("Saving manifest to {}".format(self._manifest_path))
507+ with open(self._manifest_path, 'wt', encoding='UTF-8') as stream:
508+ json.dump(manifest_cache, stream, sort_keys=True, indent=2)
509+
510 @raises(ValueError, TypeError, UnexpectedMethodCall)
511 def run_job(
512 self, job_id: str, ui: 'Union[str, IJobRunnerUI]',
513@@ -1728,6 +1829,8 @@ class SessionAssistant:
514 self.remove_all_filters: "to remove all filters",
515 self.get_static_todo_list: "to see what is meant to be executed",
516 self.get_dynamic_todo_list: "to see what is yet to be executed",
517+ self.get_manifest_repr: (
518+ "to get participating manifest units"),
519 self.run_job: "to run a given job",
520 self.use_alternate_selection: "to change the selection",
521 self.hand_pick_jobs: "to generate new selection and use it",
522diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
523index 9b91aeb..f5eb00e 100644
524--- a/plainbox/impl/session/remote_assistant.py
525+++ b/plainbox/impl/session/remote_assistant.py
526@@ -286,6 +286,12 @@ class RemoteSessionAssistant():
527 job_state.attempts = self._launcher.max_attempts
528 return self._sa.get_static_todo_list()
529
530+ def get_manifest_repr(self):
531+ return self._sa.get_manifest_repr()
532+
533+ def save_manifest(self, manifest_answers):
534+ return self._sa.save_manifest(manifest_answers)
535+
536 def modify_todo_list(self, chosen_jobs):
537 self._sa.use_alternate_selection(chosen_jobs)
538

Subscribers

People subscribed via source and target branches