Merge ~sylvain-pineau/checkbox-ng:tp-export into checkbox-ng:master

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: d138689e1755668516c083fc6ccddeeb731e9248
Merged at revision: 4e1a267400404663bceb5f83cd0a94835a050dcb
Proposed branch: ~sylvain-pineau/checkbox-ng:tp-export
Merge into: checkbox-ng:master
Diff against target: 587 lines (+246/-29)
10 files modified
checkbox_ng/launcher/checkbox_cli.py (+3/-1)
checkbox_ng/launcher/subcommands.py (+30/-0)
docs/using.rst (+27/-0)
plainbox/impl/ctrl.py (+14/-6)
plainbox/impl/exporter/xlsx.py (+102/-13)
plainbox/impl/providers/exporters/units/exporter.pxu (+7/-0)
plainbox/impl/runner.py (+32/-0)
plainbox/impl/session/assistant.py (+13/-6)
plainbox/impl/session/state.py (+4/-2)
plainbox/impl/unit/template.py (+14/-1)
Reviewer Review Type Date Requested Status
Maciej Kisielewski (community) Approve
Sylvain Pineau (community) Needs Resubmitting
Review via email: mp+353855@code.launchpad.net

Description of the change

New command line subcommand to generate test case guide based on a given test plan unit.

Example:

$ checkbox-cli tp-export com.canonical.certification::client-cert-18-04

Or to export a PDF:

$ checkbox-cli tp-export com.canonical.certification::client-cert-18-04 | xargs -d '\n' libreoffice --headless --invisible --convert-to pdf

Sample output: https://private-fileshare.canonical.com/~spineau/18.04%20Client%20Certification%20Tests.pdf

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

I consider a bad design making the core of checkbox (JobRunner) aware of the TP export functionality and hardcoding a special case for graphics cards. If having specialised JobRunner is the best solution for the problem, let's do just that, by creating FakeJobRunner that overrides necessary parts. It could also help with the problem of propagating tp_export arg (which I would prefer renamed to exported_tp_name, tp_export misled me, but that's subjective).

I know this code does the job, but for me it has a "dirty hack" feel and voids abstractions that are already in place.

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

Corrected version this time using a special FakeJobRunner class. the tp name is no longer passed trough all layers, instead i'm passing a boolean flag, "fake_resources".

Tested using:

$ checkbox-cli tp-export com.canonical.certification::client-cert-18-04

$ checkbox-cli tp-export com.canonical.qa.plano::gen2

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

I like it much more! I also sense that we may use this approach in the future for something else :)

Thank you and +1.

One thing to consider below.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py
2index 59202e7..8457a3f 100644
3--- a/checkbox_ng/launcher/checkbox_cli.py
4+++ b/checkbox_ng/launcher/checkbox_cli.py
5@@ -40,7 +40,8 @@ from plainbox.impl.launcher import DefaultLauncherDefinition
6 from plainbox.impl.launcher import LauncherDefinition
7
8 from checkbox_ng.launcher.subcommands import (
9- Launcher, List, Run, StartProvider, Submit, ListBootstrapped
10+ Launcher, List, Run, StartProvider, Submit, ListBootstrapped,
11+ TestPlanExport
12 )
13 from checkbox_ng.launcher.check_config import CheckConfig
14 from checkbox_ng.launcher.remote import RemoteSlave, RemoteMaster
15@@ -161,6 +162,7 @@ class CheckboxCommand(CanonicalCommand):
16 ('startprovider', StartProvider),
17 ('submit', Submit),
18 ('list-bootstrapped', ListBootstrapped),
19+ ('tp-export', TestPlanExport),
20 ('slave', RemoteSlave),
21 ('master', RemoteMaster),
22 )
23diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
24index cb31211..768c682 100644
25--- a/checkbox_ng/launcher/subcommands.py
26+++ b/checkbox_ng/launcher/subcommands.py
27@@ -973,6 +973,36 @@ class ListBootstrapped(Command):
28 print(job_id)
29
30
31+class TestPlanExport(Command):
32+ name = 'tp-export'
33+
34+ @property
35+ def sa(self):
36+ return self.ctx.sa
37+
38+ def register_arguments(self, parser):
39+ parser.add_argument(
40+ 'TEST_PLAN',
41+ help=_("test-plan id to bootstrap"))
42+
43+ def invoked(self, ctx):
44+ self.ctx = ctx
45+ try_selecting_providers(self.sa, '*')
46+ from plainbox.impl.runner import FakeJobRunner
47+ self.sa.start_new_session('tp-export-ephemeral', FakeJobRunner)
48+ self.sa._context.state._fake_resources = True
49+ tps = self.sa.get_test_plans()
50+ if ctx.args.TEST_PLAN not in tps:
51+ raise SystemExit('Test plan not found')
52+ self.sa.select_test_plan(ctx.args.TEST_PLAN)
53+ self.sa.bootstrap()
54+ path = self.sa.export_to_file(
55+ 'com.canonical.plainbox::tp-export', [],
56+ self.sa._manager.storage.location,
57+ self.sa._manager.test_plans[0].name)
58+ print(path)
59+
60+
61 def try_selecting_providers(sa, *args, **kwargs):
62 """
63 Try selecting proivders via SessionAssistant.
64diff --git a/docs/using.rst b/docs/using.rst
65index cee2785..0d9bccd 100644
66--- a/docs/using.rst
67+++ b/docs/using.rst
68@@ -131,6 +131,33 @@ Similarly to the ``checkbox-cli list all-jobs`` command, the output of
69 See ``checkbox-cli list`` :ref:`output-formatting` section for more information.
70
71
72+checkbox-cli tp-export
73+``````````````````````
74+
75+``tp-export`` exports a test plan as a spreadsheet document. Tests are grouped
76+by categories and ordered alphabetically with the full description (or the job
77+summary if there's no description). I addition to the description, the
78+certification status (blocker/non-blocker) is exported.
79+
80+The session is similar to ``list-bootstrapped`` but all resource jobs are
81+returning fake objects and template-filters are disabled to ensure
82+instantiation of template units. By default only one resource object is
83+returned. The only exception is the graphics_card resource where two objects are
84+used to simulate hybrid graphics.
85+
86+The command prints the full path to the document on exit/success.
87+
88+Example::
89+
90+ $ checkbox-cli tp-export com.canonical.certification::client-cert-18-04
91+
92+It can be used to automatically generate a test case guide using a pdf converter:
93+
94+Example::
95+
96+ $ checkbox-cli tp-export com.canonical.certification::client-cert-18-04 | xargs -d '\n' libreoffice --headless --invisible --convert-to pdf
97+
98+
99 checkbox-cli launcher
100 `````````````````````
101
102diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py
103index ef1441c..471dd5f 100644
104--- a/plainbox/impl/ctrl.py
105+++ b/plainbox/impl/ctrl.py
106@@ -224,7 +224,8 @@ class CheckBoxSessionStateController(ISessionStateController):
107 inhibitors.append(inhibitor)
108 return inhibitors
109
110- def observe_result(self, session_state, job, result):
111+ def observe_result(self, session_state, job, result,
112+ fake_resources=False):
113 """
114 Notice the specified test result and update readiness state.
115
116@@ -234,6 +235,9 @@ class CheckBoxSessionStateController(ISessionStateController):
117 A JobDefinition object
118 :param result:
119 A IJobResult object
120+ :param fake_resources:
121+ An optional parameter to trigger test plan export execution mode
122+ using fake resourceobjects
123
124 This function updates the internal result collection with the data from
125 the specified test result. Results can safely override older results.
126@@ -256,15 +260,18 @@ class CheckBoxSessionStateController(ISessionStateController):
127 session_state.on_job_result_changed(job, result)
128 # Treat some jobs specially and interpret their output
129 if job.plugin == "resource":
130- self._process_resource_result(session_state, job, result)
131+ self._process_resource_result(
132+ session_state, job, result, fake_resources)
133
134- def _process_resource_result(self, session_state, job, result):
135+ def _process_resource_result(self, session_state, job, result,
136+ fake_resources=False):
137 """
138 Analyze a result of a CheckBox "resource" job and generate
139 or replace resource records.
140 """
141 self._parse_and_store_resource(session_state, job, result)
142- self._instantiate_templates(session_state, job, result)
143+ self._instantiate_templates(
144+ session_state, job, result, fake_resources)
145
146 def _parse_and_store_resource(self, session_state, job, result):
147 # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928
148@@ -289,7 +296,8 @@ class CheckBoxSessionStateController(ISessionStateController):
149 # Replace any old resources with the new resource list
150 session_state.set_resource_list(job.id, new_resource_list)
151
152- def _instantiate_templates(self, session_state, job, result):
153+ def _instantiate_templates(self, session_state, job, result,
154+ fake_resources=False):
155 # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928
156 # If we are resuming from a session that had a resource job that
157 # never ran, we will see an empty MemoryJobResult object.
158@@ -302,7 +310,7 @@ class CheckBoxSessionStateController(ISessionStateController):
159 if isinstance(unit, TemplateUnit) and unit.resource_id == job.id:
160 logger.info(_("Instantiating unit: %s"), unit)
161 for new_unit in unit.instantiate_all(
162- session_state.resource_map[job.id]):
163+ session_state.resource_map[job.id], fake_resources):
164 try:
165 check_result = new_unit.check()
166 except MissingParam as m:
167diff --git a/plainbox/impl/exporter/xlsx.py b/plainbox/impl/exporter/xlsx.py
168index 360a368..f582339 100644
169--- a/plainbox/impl/exporter/xlsx.py
170+++ b/plainbox/impl/exporter/xlsx.py
171@@ -64,12 +64,14 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
172 OPTION_WITH_SUMMARY = 'with-summary'
173 OPTION_WITH_DESCRIPTION = 'with-job-description'
174 OPTION_WITH_TEXT_ATTACHMENTS = 'with-text-attachments'
175+ OPTION_TEST_PLAN_EXPORT = 'tp-export'
176
177 SUPPORTED_OPTION_LIST = (
178 OPTION_WITH_SYSTEM_INFO,
179 OPTION_WITH_SUMMARY,
180 OPTION_WITH_DESCRIPTION,
181 OPTION_WITH_TEXT_ATTACHMENTS,
182+ OPTION_TEST_PLAN_EXPORT,
183 )
184
185 def __init__(self, option_list=None, exporter_unit=None):
186@@ -135,6 +137,10 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
187 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
188 'border': 1, 'bg_color': '#E6E6E6',
189 })
190+ self.format06_2 = self.workbook.add_format({
191+ 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
192+ 'border': 1, 'bg_color': '#E6E6E6', 'bold': 1,
193+ })
194 # Headlines (center)
195 self.format07 = self.workbook.add_format({
196 'align': 'center', 'size': 10, 'bold': 1,
197@@ -385,7 +391,8 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
198 self.format08 if i % 2 else self.format09
199 )
200 self.worksheet1.set_row(
201- packages_starting_row + i, None, None, {'level': 1, 'hidden': True}
202+ packages_starting_row + i, None, None,
203+ {'level': 1, 'hidden': True}
204 )
205 self.worksheet1.set_row(
206 packages_starting_row+len(data["resource_map"][resource]),
207@@ -522,11 +529,11 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
208 self.worksheet4.set_row(
209 self._lineno, 13, None, {'level': level})
210 else:
211- self.worksheet3.set_row(self._lineno, 13, None,
212- {'collapsed': True})
213+ self.worksheet3.set_row(
214+ self._lineno, 13, None, {'collapsed': True})
215 if self.OPTION_WITH_DESCRIPTION in self._option_list:
216- self.worksheet4.set_row(self._lineno, 13, None,
217- {'collapsed': True})
218+ self.worksheet4.set_row(
219+ self._lineno, 13, None, {'collapsed': True})
220 self._write_job(children, result_map, max_level, level + 1)
221 else:
222 self.worksheet3.write(
223@@ -604,8 +611,9 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
224 self._lineno, 12 + 10.5 * desc_lines,
225 None, {'level': level, 'hidden': True})
226 else:
227- self.worksheet3.set_row(self._lineno, 12 + 10.5 * io_lines,
228- None, {'hidden': True})
229+ self.worksheet3.set_row(
230+ self._lineno, 12 + 10.5 * io_lines,
231+ None, {'hidden': True})
232 if self.OPTION_WITH_DESCRIPTION in self._option_list:
233 self.worksheet4.set_row(
234 self._lineno, 12 + 10.5 * desc_lines, None,
235@@ -649,6 +657,63 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
236 self._lineno + 1, None, None, {'collapsed': True})
237 self.worksheet3.autofilter(5, max_level, self._lineno, max_level + 3)
238
239+ def write_tp_export(self, data):
240+ def _category_map(state):
241+ """Map from category id to their corresponding translated names."""
242+ wanted_category_ids = frozenset({
243+ job_state.effective_category_id
244+ for job_state in state.job_state_map.values()
245+ if job_state.job in state.run_list and
246+ job_state.job.plugin not in ("resource", "attachment")
247+ })
248+ return {
249+ unit.id: unit.tr_name()
250+ for unit in state.unit_list
251+ if unit.Meta.name == 'category'
252+ and unit.id in wanted_category_ids
253+ }
254+ self.worksheet4.set_header(
255+ '&C{}'.format(data['manager'].test_plans[0]))
256+ self.worksheet4.set_footer('&CPage &P of &N')
257+ self.worksheet4.set_margins(left=0.3, right=0.3, top=0.5, bottom=0.5)
258+ self.worksheet4.set_column(0, 0, 40)
259+ self.worksheet4.set_column(1, 1, 13)
260+ self.worksheet4.set_column(2, 2, 55)
261+ self.worksheet4.write_row(
262+ 0, 0,
263+ ['Name', 'Certification status', 'Description'], self.format06_2
264+ )
265+ self.worksheet4.repeat_rows(0)
266+ self._lineno = 0
267+ state = data['manager'].default_device_context.state
268+ cat_map = _category_map(state)
269+ run_list_ids = [job.id for job in state.run_list]
270+ for cat_id in sorted(cat_map, key=lambda x: cat_map[x].casefold()):
271+ for job_id in sorted(state._job_state_map):
272+ job_state = state._job_state_map[job_id]
273+ if job_id not in run_list_ids:
274+ continue
275+ if (
276+ job_state.effective_category_id == cat_id and
277+ job_state.job.plugin not in ("resource", "attachment")
278+ ):
279+ self._lineno += 1
280+ certification_status = \
281+ job_state.effective_certification_status
282+ if certification_status == 'unspecified':
283+ certification_status = ''
284+ description = job_state.job.description
285+ if not description:
286+ description = job_state.job.summary
287+ self.worksheet4.write_row(
288+ self._lineno, 0,
289+ [job_state.job.partial_id,
290+ certification_status, description],
291+ self.format05)
292+ desc_lines = len(description.splitlines()) + 1
293+ self.worksheet4.set_row(self._lineno, 12 * desc_lines)
294+ self._lineno += 1
295+
296 def write_attachments(self, data):
297 self.worksheet5.set_column(0, 0, 5)
298 self.worksheet5.set_column(1, 1, 120)
299@@ -679,7 +744,8 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
300 self.worksheet6.set_column(0, 0, 5)
301 self.worksheet6.set_column(1, 1, 120)
302 i = 4
303- for name in [job_id for job_id in data['result_map'] if data['result_map'][job_id]['plugin'] == 'resource']:
304+ for name in [job_id for job_id in data['result_map']
305+ if data['result_map'][job_id]['plugin'] == 'resource']:
306 io_log = ' '
307 try:
308 if data['result_map'][name]['io_log']:
309@@ -704,6 +770,21 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
310 self.worksheet6.set_row(i + j, None, None, {'collapsed': True})
311 i += j + 1 # Insert a newline between resources logs
312
313+ def dump_from_session_manager(self, session_manager, stream):
314+ """
315+ Extract data from session_manager and dump it into the stream.
316+
317+ :param session_manager:
318+ SessionManager instance that manages session to be exported by
319+ this exporter
320+ :param stream:
321+ Byte stream to write to.
322+
323+ """
324+ data = self.get_session_data_subset(session_manager)
325+ data['manager'] = session_manager
326+ self.dump(data, stream)
327+
328 def dump(self, data, stream):
329 """
330 Public method to dump the XLSX report to a stream
331@@ -713,19 +794,27 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
332 if self.OPTION_WITH_SYSTEM_INFO in self._option_list:
333 self.worksheet1 = self.workbook.add_worksheet(_('System Info'))
334 self.write_systeminfo(data)
335- self.worksheet3 = self.workbook.add_worksheet(_('Test Results'))
336- if self.OPTION_WITH_DESCRIPTION in self._option_list:
337+ if not self.OPTION_TEST_PLAN_EXPORT:
338+ self.worksheet3 = self.workbook.add_worksheet(_('Test Results'))
339+ if (
340+ self.OPTION_WITH_DESCRIPTION in self._option_list or
341+ self.OPTION_TEST_PLAN_EXPORT in self._option_list
342+ ):
343 self.worksheet4 = self.workbook.add_worksheet(
344 _('Test Descriptions'))
345- self.write_results(data)
346+ if self.OPTION_TEST_PLAN_EXPORT:
347+ self.write_tp_export(data)
348+ else:
349+ self.write_results(data)
350 if self.OPTION_WITH_SUMMARY in self._option_list:
351 self.worksheet2 = self.workbook.add_worksheet(_('Summary'))
352 self.write_summary(data)
353 if self.OPTION_WITH_TEXT_ATTACHMENTS in self._option_list:
354 self.worksheet5 = self.workbook.add_worksheet(_('Log Files'))
355 self.write_attachments(data)
356- self.worksheet6 = self.workbook.add_worksheet(_('Resources Logs'))
357- self.write_resources(data)
358+ if not self.OPTION_TEST_PLAN_EXPORT:
359+ self.worksheet6 = self.workbook.add_worksheet(_('Resources Logs'))
360+ self.write_resources(data)
361 for worksheet in self.workbook.worksheets():
362 worksheet.outline_settings(True, False, False, True)
363 worksheet.hide_gridlines(2)
364diff --git a/plainbox/impl/providers/exporters/units/exporter.pxu b/plainbox/impl/providers/exporters/units/exporter.pxu
365index 65f96eb..c55843f 100644
366--- a/plainbox/impl/providers/exporters/units/exporter.pxu
367+++ b/plainbox/impl/providers/exporters/units/exporter.pxu
368@@ -45,3 +45,10 @@ _summary: Generate junit XML
369 entry_point: jinja2
370 file_extension: junit.xml
371 data: {"template": "junit.xml"}
372+
373+unit: exporter
374+id: tp-export
375+_summary: Generate an Excel 2007+ XLSX export of a test plan
376+entry_point: xlsx
377+file_extension: xlsx
378+options: tp-export
379\ No newline at end of file
380diff --git a/plainbox/impl/runner.py b/plainbox/impl/runner.py
381index 8adf4dc..7807a8f 100644
382--- a/plainbox/impl/runner.py
383+++ b/plainbox/impl/runner.py
384@@ -518,6 +518,7 @@ class JobRunner(IJobRunner):
385 else:
386 result = self._just_run_command(
387 job, job_state, config).get_result()
388+
389 return result
390
391 def run_manual_job(self, job, job_state, config):
392@@ -970,3 +971,34 @@ class JobRunner(IJobRunner):
393 logger.warning(
394 _("Please store desired files in $PLAINBOX_SESSION_SHARE and"
395 " use regular temporary files for everything else"))
396+
397+
398+class FakeJobRunner(JobRunner):
399+
400+ """
401+ Fake runner for jobs.
402+
403+ Special runner that creates fake resource objects.
404+ """
405+
406+ def run_resource_job(self, job, job_state, config):
407+ """
408+ Method called to run a job with plugin field equal to 'resource'.
409+
410+ Only one resouce object is created from this runner.
411+ Exception: 'graphics_card' resource job creates two objects to
412+ simulate hybrid graphics.
413+ """
414+ if job.plugin != "resource":
415+ # TRANSLATORS: please keep 'plugin' untranslated
416+ raise ValueError(_("bad job plugin value"))
417+ builder = JobResultBuilder()
418+ if job.partial_id == 'graphics_card':
419+ builder.io_log = [(0, 'stdout', b'a: b\n'),
420+ (1, 'stdout', b'\n'),
421+ (2, 'stdout', b'a: c\n')]
422+ else:
423+ builder.io_log = [(0, 'stdout', b'a: b\n')]
424+ builder.outcome = 'pass'
425+ builder.return_code = 0
426+ return builder.get_result()
427diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
428index 9470905..995e38b 100644
429--- a/plainbox/impl/session/assistant.py
430+++ b/plainbox/impl/session/assistant.py
431@@ -551,7 +551,7 @@ class SessionAssistant:
432 storage.remove()
433
434 @raises(UnexpectedMethodCall)
435- def start_new_session(self, title: str):
436+ def start_new_session(self, title: str, runner_cls=JobRunner):
437 """
438 Create a new testing session.
439
440@@ -588,7 +588,7 @@ class SessionAssistant:
441 self._metadata.flags = {'bootstrapping'}
442 self._manager.checkpoint()
443 self._command_io_delegate = JobRunnerUIDelegate(_SilentUI())
444- self._init_runner()
445+ self._init_runner(runner_cls)
446 self.session_available(self._manager.storage.id)
447 _logger.debug("New session created: %s", title)
448 UsageExpectation.of(self).allowed_calls = {
449@@ -1541,7 +1541,8 @@ class SessionAssistant:
450
451 @raises(KeyError, OSError)
452 def export_to_file(
453- self, exporter_id: str, option_list: 'list[str]', dir_path: str
454+ self, exporter_id: str, option_list: 'list[str]', dir_path: str,
455+ filename: str=None
456 ) -> str:
457 """
458 Export the session to file using given exporter ID.
459@@ -1556,6 +1557,9 @@ class SessionAssistant:
460 :param dir_path:
461 Path to the directory where session file should be written to.
462 Note that the file name is automatically generated, based on
463+ :param filename:
464+ Optional file name (without extension)
465+ By default, the file name is automatically generated, based on
466 creation time and type of exporter.
467 :returns:
468 Path to the written file.
469@@ -1572,8 +1576,11 @@ class SessionAssistant:
470 # issues when copying files.
471 isoformat = "%Y-%m-%dT%H.%M.%S.%f"
472 timestamp = datetime.datetime.utcnow().strftime(isoformat)
473+ basename = 'submission_' + timestamp
474+ if filename:
475+ basename = filename
476 path = os.path.join(dir_path, ''.join(
477- ['submission_', timestamp, '.', exporter.unit.file_extension]))
478+ [basename, '.', exporter.unit.file_extension]))
479 with open(path, 'wb') as stream:
480 exporter.dump_from_session_manager(self._manager, stream)
481 return path
482@@ -1663,12 +1670,12 @@ class SessionAssistant:
483 self.finish_bootstrap: "to finish bootstrapping",
484 }
485
486- def _init_runner(self):
487+ def _init_runner(self, runner_cls):
488 self._execution_ctrl_list = []
489 for ctrl_cls, args, kwargs in self._ctrl_setup_list:
490 self._execution_ctrl_list.append(
491 ctrl_cls(self._context.provider_list, *args, **kwargs))
492- self._runner = JobRunner(
493+ self._runner = runner_cls(
494 self._manager.storage.location,
495 self._context.provider_list,
496 jobs_io_log_dir=os.path.join(
497diff --git a/plainbox/impl/session/state.py b/plainbox/impl/session/state.py
498index 9360f91..9dfc15a 100644
499--- a/plainbox/impl/session/state.py
500+++ b/plainbox/impl/session/state.py
501@@ -799,6 +799,7 @@ class SessionState:
502 self._mandatory_job_list = []
503 self._run_list = []
504 self._resource_map = {}
505+ self._fake_resources = False
506 self._metadata = SessionMetaData()
507 super(SessionState, self).__init__()
508
509@@ -998,7 +999,8 @@ class SessionState:
510 any old entries), with a list of the resources that were parsed from
511 the IO log.
512 """
513- job.controller.observe_result(self, job, result)
514+ job.controller.observe_result(
515+ self, job, result, fake_resources=self._fake_resources)
516 self._recompute_job_readiness()
517
518 @deprecated('0.9', 'use the add_unit() method instead')
519@@ -1100,7 +1102,7 @@ class SessionState:
520 else:
521 # If there is a clash report DependencyDuplicateError only when the
522 # hashes are different.
523- if new_job != existing_job:
524+ if new_job != existing_job and not self._fake_resources:
525 raise DependencyDuplicateError(existing_job, new_job)
526 self._add_job_siblings_unit(new_job, recompute, via)
527 return existing_job
528diff --git a/plainbox/impl/unit/template.py b/plainbox/impl/unit/template.py
529index 6e8a810..1103a76 100644
530--- a/plainbox/impl/unit/template.py
531+++ b/plainbox/impl/unit/template.py
532@@ -139,6 +139,7 @@ class TemplateUnit(Unit):
533 super().__init__(
534 data, raw_data, origin, provider, parameters, field_offset_map)
535 self._filter_program = None
536+ self._fake_resources = False
537
538 @classmethod
539 def instantiate_template(cls, data, raw_data, origin, provider, parameters,
540@@ -302,7 +303,7 @@ class TemplateUnit(Unit):
541 all_units.load()
542 return all_units.get_by_name(self.template_unit).plugin_object
543
544- def instantiate_all(self, resource_list):
545+ def instantiate_all(self, resource_list, fake_resources=False):
546 """
547 Instantiate a list of job definitions.
548
549@@ -311,12 +312,15 @@ class TemplateUnit(Unit):
550 :param resource_list:
551 A list of resource objects with the correct name
552 (:meth:`template_resource`)
553+ :param fake_resources:
554+ An optional parameter to trigger test plan export execution mode
555 :returns:
556 A list of new Unit (or subclass) objects.
557 """
558 unit_cls = self.get_target_unit_cls()
559 resources = []
560 index = 0
561+ self._fake_resources = fake_resources
562 for resource in resource_list:
563 if self.should_instantiate(resource):
564 index += 1
565@@ -380,6 +384,13 @@ class TemplateUnit(Unit):
566 # See https://bugs.launchpad.net/bugs/1561821
567 parameters = {
568 k: v for k, v in parameters.items() if k in accessed_parameters}
569+ if self._fake_resources:
570+ parameters = {k: k.upper() for k in accessed_parameters}
571+ for k in parameters:
572+ if k.endswith('_slug'):
573+ parameters[k] = k.replace('_slug', '').upper()
574+ if 'index' in parameters:
575+ parameters['index'] = index
576 # Add the special __index__ to the resource namespace variables
577 parameters['__index__'] = index
578 # Instantiate the class using the instantiation API
579@@ -401,6 +412,8 @@ class TemplateUnit(Unit):
580 specified resource object would make the filter program evaluate to
581 True.
582 """
583+ if self._fake_resources:
584+ return True
585 program = self.get_filter_program()
586 if program is None:
587 return True

Subscribers

People subscribed via source and target branches