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
diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py
index 59202e7..8457a3f 100644
--- a/checkbox_ng/launcher/checkbox_cli.py
+++ b/checkbox_ng/launcher/checkbox_cli.py
@@ -40,7 +40,8 @@ from plainbox.impl.launcher import DefaultLauncherDefinition
40from plainbox.impl.launcher import LauncherDefinition40from plainbox.impl.launcher import LauncherDefinition
4141
42from checkbox_ng.launcher.subcommands import (42from checkbox_ng.launcher.subcommands import (
43 Launcher, List, Run, StartProvider, Submit, ListBootstrapped43 Launcher, List, Run, StartProvider, Submit, ListBootstrapped,
44 TestPlanExport
44)45)
45from checkbox_ng.launcher.check_config import CheckConfig46from checkbox_ng.launcher.check_config import CheckConfig
46from checkbox_ng.launcher.remote import RemoteSlave, RemoteMaster47from checkbox_ng.launcher.remote import RemoteSlave, RemoteMaster
@@ -161,6 +162,7 @@ class CheckboxCommand(CanonicalCommand):
161 ('startprovider', StartProvider),162 ('startprovider', StartProvider),
162 ('submit', Submit),163 ('submit', Submit),
163 ('list-bootstrapped', ListBootstrapped),164 ('list-bootstrapped', ListBootstrapped),
165 ('tp-export', TestPlanExport),
164 ('slave', RemoteSlave),166 ('slave', RemoteSlave),
165 ('master', RemoteMaster),167 ('master', RemoteMaster),
166 )168 )
diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
index cb31211..768c682 100644
--- a/checkbox_ng/launcher/subcommands.py
+++ b/checkbox_ng/launcher/subcommands.py
@@ -973,6 +973,36 @@ class ListBootstrapped(Command):
973 print(job_id)973 print(job_id)
974974
975975
976class TestPlanExport(Command):
977 name = 'tp-export'
978
979 @property
980 def sa(self):
981 return self.ctx.sa
982
983 def register_arguments(self, parser):
984 parser.add_argument(
985 'TEST_PLAN',
986 help=_("test-plan id to bootstrap"))
987
988 def invoked(self, ctx):
989 self.ctx = ctx
990 try_selecting_providers(self.sa, '*')
991 from plainbox.impl.runner import FakeJobRunner
992 self.sa.start_new_session('tp-export-ephemeral', FakeJobRunner)
993 self.sa._context.state._fake_resources = True
994 tps = self.sa.get_test_plans()
995 if ctx.args.TEST_PLAN not in tps:
996 raise SystemExit('Test plan not found')
997 self.sa.select_test_plan(ctx.args.TEST_PLAN)
998 self.sa.bootstrap()
999 path = self.sa.export_to_file(
1000 'com.canonical.plainbox::tp-export', [],
1001 self.sa._manager.storage.location,
1002 self.sa._manager.test_plans[0].name)
1003 print(path)
1004
1005
976def try_selecting_providers(sa, *args, **kwargs):1006def try_selecting_providers(sa, *args, **kwargs):
977 """1007 """
978 Try selecting proivders via SessionAssistant.1008 Try selecting proivders via SessionAssistant.
diff --git a/docs/using.rst b/docs/using.rst
index cee2785..0d9bccd 100644
--- a/docs/using.rst
+++ b/docs/using.rst
@@ -131,6 +131,33 @@ Similarly to the ``checkbox-cli list all-jobs`` command, the output of
131See ``checkbox-cli list`` :ref:`output-formatting` section for more information.131See ``checkbox-cli list`` :ref:`output-formatting` section for more information.
132132
133133
134checkbox-cli tp-export
135``````````````````````
136
137``tp-export`` exports a test plan as a spreadsheet document. Tests are grouped
138by categories and ordered alphabetically with the full description (or the job
139summary if there's no description). I addition to the description, the
140certification status (blocker/non-blocker) is exported.
141
142The session is similar to ``list-bootstrapped`` but all resource jobs are
143returning fake objects and template-filters are disabled to ensure
144instantiation of template units. By default only one resource object is
145returned. The only exception is the graphics_card resource where two objects are
146used to simulate hybrid graphics.
147
148The command prints the full path to the document on exit/success.
149
150Example::
151
152 $ checkbox-cli tp-export com.canonical.certification::client-cert-18-04
153
154It can be used to automatically generate a test case guide using a pdf converter:
155
156Example::
157
158 $ checkbox-cli tp-export com.canonical.certification::client-cert-18-04 | xargs -d '\n' libreoffice --headless --invisible --convert-to pdf
159
160
134checkbox-cli launcher161checkbox-cli launcher
135`````````````````````162`````````````````````
136163
diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py
index ef1441c..471dd5f 100644
--- a/plainbox/impl/ctrl.py
+++ b/plainbox/impl/ctrl.py
@@ -224,7 +224,8 @@ class CheckBoxSessionStateController(ISessionStateController):
224 inhibitors.append(inhibitor)224 inhibitors.append(inhibitor)
225 return inhibitors225 return inhibitors
226226
227 def observe_result(self, session_state, job, result):227 def observe_result(self, session_state, job, result,
228 fake_resources=False):
228 """229 """
229 Notice the specified test result and update readiness state.230 Notice the specified test result and update readiness state.
230231
@@ -234,6 +235,9 @@ class CheckBoxSessionStateController(ISessionStateController):
234 A JobDefinition object235 A JobDefinition object
235 :param result:236 :param result:
236 A IJobResult object237 A IJobResult object
238 :param fake_resources:
239 An optional parameter to trigger test plan export execution mode
240 using fake resourceobjects
237241
238 This function updates the internal result collection with the data from242 This function updates the internal result collection with the data from
239 the specified test result. Results can safely override older results.243 the specified test result. Results can safely override older results.
@@ -256,15 +260,18 @@ class CheckBoxSessionStateController(ISessionStateController):
256 session_state.on_job_result_changed(job, result)260 session_state.on_job_result_changed(job, result)
257 # Treat some jobs specially and interpret their output261 # Treat some jobs specially and interpret their output
258 if job.plugin == "resource":262 if job.plugin == "resource":
259 self._process_resource_result(session_state, job, result)263 self._process_resource_result(
264 session_state, job, result, fake_resources)
260265
261 def _process_resource_result(self, session_state, job, result):266 def _process_resource_result(self, session_state, job, result,
267 fake_resources=False):
262 """268 """
263 Analyze a result of a CheckBox "resource" job and generate269 Analyze a result of a CheckBox "resource" job and generate
264 or replace resource records.270 or replace resource records.
265 """271 """
266 self._parse_and_store_resource(session_state, job, result)272 self._parse_and_store_resource(session_state, job, result)
267 self._instantiate_templates(session_state, job, result)273 self._instantiate_templates(
274 session_state, job, result, fake_resources)
268275
269 def _parse_and_store_resource(self, session_state, job, result):276 def _parse_and_store_resource(self, session_state, job, result):
270 # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928277 # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928
@@ -289,7 +296,8 @@ class CheckBoxSessionStateController(ISessionStateController):
289 # Replace any old resources with the new resource list296 # Replace any old resources with the new resource list
290 session_state.set_resource_list(job.id, new_resource_list)297 session_state.set_resource_list(job.id, new_resource_list)
291298
292 def _instantiate_templates(self, session_state, job, result):299 def _instantiate_templates(self, session_state, job, result,
300 fake_resources=False):
293 # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928301 # NOTE: https://bugs.launchpad.net/checkbox/+bug/1297928
294 # If we are resuming from a session that had a resource job that302 # If we are resuming from a session that had a resource job that
295 # never ran, we will see an empty MemoryJobResult object.303 # never ran, we will see an empty MemoryJobResult object.
@@ -302,7 +310,7 @@ class CheckBoxSessionStateController(ISessionStateController):
302 if isinstance(unit, TemplateUnit) and unit.resource_id == job.id:310 if isinstance(unit, TemplateUnit) and unit.resource_id == job.id:
303 logger.info(_("Instantiating unit: %s"), unit)311 logger.info(_("Instantiating unit: %s"), unit)
304 for new_unit in unit.instantiate_all(312 for new_unit in unit.instantiate_all(
305 session_state.resource_map[job.id]):313 session_state.resource_map[job.id], fake_resources):
306 try:314 try:
307 check_result = new_unit.check()315 check_result = new_unit.check()
308 except MissingParam as m:316 except MissingParam as m:
diff --git a/plainbox/impl/exporter/xlsx.py b/plainbox/impl/exporter/xlsx.py
index 360a368..f582339 100644
--- a/plainbox/impl/exporter/xlsx.py
+++ b/plainbox/impl/exporter/xlsx.py
@@ -64,12 +64,14 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
64 OPTION_WITH_SUMMARY = 'with-summary'64 OPTION_WITH_SUMMARY = 'with-summary'
65 OPTION_WITH_DESCRIPTION = 'with-job-description'65 OPTION_WITH_DESCRIPTION = 'with-job-description'
66 OPTION_WITH_TEXT_ATTACHMENTS = 'with-text-attachments'66 OPTION_WITH_TEXT_ATTACHMENTS = 'with-text-attachments'
67 OPTION_TEST_PLAN_EXPORT = 'tp-export'
6768
68 SUPPORTED_OPTION_LIST = (69 SUPPORTED_OPTION_LIST = (
69 OPTION_WITH_SYSTEM_INFO,70 OPTION_WITH_SYSTEM_INFO,
70 OPTION_WITH_SUMMARY,71 OPTION_WITH_SUMMARY,
71 OPTION_WITH_DESCRIPTION,72 OPTION_WITH_DESCRIPTION,
72 OPTION_WITH_TEXT_ATTACHMENTS,73 OPTION_WITH_TEXT_ATTACHMENTS,
74 OPTION_TEST_PLAN_EXPORT,
73 )75 )
7476
75 def __init__(self, option_list=None, exporter_unit=None):77 def __init__(self, option_list=None, exporter_unit=None):
@@ -135,6 +137,10 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
135 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,137 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
136 'border': 1, 'bg_color': '#E6E6E6',138 'border': 1, 'bg_color': '#E6E6E6',
137 })139 })
140 self.format06_2 = self.workbook.add_format({
141 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8,
142 'border': 1, 'bg_color': '#E6E6E6', 'bold': 1,
143 })
138 # Headlines (center)144 # Headlines (center)
139 self.format07 = self.workbook.add_format({145 self.format07 = self.workbook.add_format({
140 'align': 'center', 'size': 10, 'bold': 1,146 'align': 'center', 'size': 10, 'bold': 1,
@@ -385,7 +391,8 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
385 self.format08 if i % 2 else self.format09391 self.format08 if i % 2 else self.format09
386 )392 )
387 self.worksheet1.set_row(393 self.worksheet1.set_row(
388 packages_starting_row + i, None, None, {'level': 1, 'hidden': True}394 packages_starting_row + i, None, None,
395 {'level': 1, 'hidden': True}
389 )396 )
390 self.worksheet1.set_row(397 self.worksheet1.set_row(
391 packages_starting_row+len(data["resource_map"][resource]),398 packages_starting_row+len(data["resource_map"][resource]),
@@ -522,11 +529,11 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
522 self.worksheet4.set_row(529 self.worksheet4.set_row(
523 self._lineno, 13, None, {'level': level})530 self._lineno, 13, None, {'level': level})
524 else:531 else:
525 self.worksheet3.set_row(self._lineno, 13, None,532 self.worksheet3.set_row(
526 {'collapsed': True})533 self._lineno, 13, None, {'collapsed': True})
527 if self.OPTION_WITH_DESCRIPTION in self._option_list:534 if self.OPTION_WITH_DESCRIPTION in self._option_list:
528 self.worksheet4.set_row(self._lineno, 13, None,535 self.worksheet4.set_row(
529 {'collapsed': True})536 self._lineno, 13, None, {'collapsed': True})
530 self._write_job(children, result_map, max_level, level + 1)537 self._write_job(children, result_map, max_level, level + 1)
531 else:538 else:
532 self.worksheet3.write(539 self.worksheet3.write(
@@ -604,8 +611,9 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
604 self._lineno, 12 + 10.5 * desc_lines,611 self._lineno, 12 + 10.5 * desc_lines,
605 None, {'level': level, 'hidden': True})612 None, {'level': level, 'hidden': True})
606 else:613 else:
607 self.worksheet3.set_row(self._lineno, 12 + 10.5 * io_lines,614 self.worksheet3.set_row(
608 None, {'hidden': True})615 self._lineno, 12 + 10.5 * io_lines,
616 None, {'hidden': True})
609 if self.OPTION_WITH_DESCRIPTION in self._option_list:617 if self.OPTION_WITH_DESCRIPTION in self._option_list:
610 self.worksheet4.set_row(618 self.worksheet4.set_row(
611 self._lineno, 12 + 10.5 * desc_lines, None,619 self._lineno, 12 + 10.5 * desc_lines, None,
@@ -649,6 +657,63 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
649 self._lineno + 1, None, None, {'collapsed': True})657 self._lineno + 1, None, None, {'collapsed': True})
650 self.worksheet3.autofilter(5, max_level, self._lineno, max_level + 3)658 self.worksheet3.autofilter(5, max_level, self._lineno, max_level + 3)
651659
660 def write_tp_export(self, data):
661 def _category_map(state):
662 """Map from category id to their corresponding translated names."""
663 wanted_category_ids = frozenset({
664 job_state.effective_category_id
665 for job_state in state.job_state_map.values()
666 if job_state.job in state.run_list and
667 job_state.job.plugin not in ("resource", "attachment")
668 })
669 return {
670 unit.id: unit.tr_name()
671 for unit in state.unit_list
672 if unit.Meta.name == 'category'
673 and unit.id in wanted_category_ids
674 }
675 self.worksheet4.set_header(
676 '&C{}'.format(data['manager'].test_plans[0]))
677 self.worksheet4.set_footer('&CPage &P of &N')
678 self.worksheet4.set_margins(left=0.3, right=0.3, top=0.5, bottom=0.5)
679 self.worksheet4.set_column(0, 0, 40)
680 self.worksheet4.set_column(1, 1, 13)
681 self.worksheet4.set_column(2, 2, 55)
682 self.worksheet4.write_row(
683 0, 0,
684 ['Name', 'Certification status', 'Description'], self.format06_2
685 )
686 self.worksheet4.repeat_rows(0)
687 self._lineno = 0
688 state = data['manager'].default_device_context.state
689 cat_map = _category_map(state)
690 run_list_ids = [job.id for job in state.run_list]
691 for cat_id in sorted(cat_map, key=lambda x: cat_map[x].casefold()):
692 for job_id in sorted(state._job_state_map):
693 job_state = state._job_state_map[job_id]
694 if job_id not in run_list_ids:
695 continue
696 if (
697 job_state.effective_category_id == cat_id and
698 job_state.job.plugin not in ("resource", "attachment")
699 ):
700 self._lineno += 1
701 certification_status = \
702 job_state.effective_certification_status
703 if certification_status == 'unspecified':
704 certification_status = ''
705 description = job_state.job.description
706 if not description:
707 description = job_state.job.summary
708 self.worksheet4.write_row(
709 self._lineno, 0,
710 [job_state.job.partial_id,
711 certification_status, description],
712 self.format05)
713 desc_lines = len(description.splitlines()) + 1
714 self.worksheet4.set_row(self._lineno, 12 * desc_lines)
715 self._lineno += 1
716
652 def write_attachments(self, data):717 def write_attachments(self, data):
653 self.worksheet5.set_column(0, 0, 5)718 self.worksheet5.set_column(0, 0, 5)
654 self.worksheet5.set_column(1, 1, 120)719 self.worksheet5.set_column(1, 1, 120)
@@ -679,7 +744,8 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
679 self.worksheet6.set_column(0, 0, 5)744 self.worksheet6.set_column(0, 0, 5)
680 self.worksheet6.set_column(1, 1, 120)745 self.worksheet6.set_column(1, 1, 120)
681 i = 4746 i = 4
682 for name in [job_id for job_id in data['result_map'] if data['result_map'][job_id]['plugin'] == 'resource']:747 for name in [job_id for job_id in data['result_map']
748 if data['result_map'][job_id]['plugin'] == 'resource']:
683 io_log = ' '749 io_log = ' '
684 try:750 try:
685 if data['result_map'][name]['io_log']:751 if data['result_map'][name]['io_log']:
@@ -704,6 +770,21 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
704 self.worksheet6.set_row(i + j, None, None, {'collapsed': True})770 self.worksheet6.set_row(i + j, None, None, {'collapsed': True})
705 i += j + 1 # Insert a newline between resources logs771 i += j + 1 # Insert a newline between resources logs
706772
773 def dump_from_session_manager(self, session_manager, stream):
774 """
775 Extract data from session_manager and dump it into the stream.
776
777 :param session_manager:
778 SessionManager instance that manages session to be exported by
779 this exporter
780 :param stream:
781 Byte stream to write to.
782
783 """
784 data = self.get_session_data_subset(session_manager)
785 data['manager'] = session_manager
786 self.dump(data, stream)
787
707 def dump(self, data, stream):788 def dump(self, data, stream):
708 """789 """
709 Public method to dump the XLSX report to a stream790 Public method to dump the XLSX report to a stream
@@ -713,19 +794,27 @@ class XLSXSessionStateExporter(SessionStateExporterBase):
713 if self.OPTION_WITH_SYSTEM_INFO in self._option_list:794 if self.OPTION_WITH_SYSTEM_INFO in self._option_list:
714 self.worksheet1 = self.workbook.add_worksheet(_('System Info'))795 self.worksheet1 = self.workbook.add_worksheet(_('System Info'))
715 self.write_systeminfo(data)796 self.write_systeminfo(data)
716 self.worksheet3 = self.workbook.add_worksheet(_('Test Results'))797 if not self.OPTION_TEST_PLAN_EXPORT:
717 if self.OPTION_WITH_DESCRIPTION in self._option_list:798 self.worksheet3 = self.workbook.add_worksheet(_('Test Results'))
799 if (
800 self.OPTION_WITH_DESCRIPTION in self._option_list or
801 self.OPTION_TEST_PLAN_EXPORT in self._option_list
802 ):
718 self.worksheet4 = self.workbook.add_worksheet(803 self.worksheet4 = self.workbook.add_worksheet(
719 _('Test Descriptions'))804 _('Test Descriptions'))
720 self.write_results(data)805 if self.OPTION_TEST_PLAN_EXPORT:
806 self.write_tp_export(data)
807 else:
808 self.write_results(data)
721 if self.OPTION_WITH_SUMMARY in self._option_list:809 if self.OPTION_WITH_SUMMARY in self._option_list:
722 self.worksheet2 = self.workbook.add_worksheet(_('Summary'))810 self.worksheet2 = self.workbook.add_worksheet(_('Summary'))
723 self.write_summary(data)811 self.write_summary(data)
724 if self.OPTION_WITH_TEXT_ATTACHMENTS in self._option_list:812 if self.OPTION_WITH_TEXT_ATTACHMENTS in self._option_list:
725 self.worksheet5 = self.workbook.add_worksheet(_('Log Files'))813 self.worksheet5 = self.workbook.add_worksheet(_('Log Files'))
726 self.write_attachments(data)814 self.write_attachments(data)
727 self.worksheet6 = self.workbook.add_worksheet(_('Resources Logs'))815 if not self.OPTION_TEST_PLAN_EXPORT:
728 self.write_resources(data)816 self.worksheet6 = self.workbook.add_worksheet(_('Resources Logs'))
817 self.write_resources(data)
729 for worksheet in self.workbook.worksheets():818 for worksheet in self.workbook.worksheets():
730 worksheet.outline_settings(True, False, False, True)819 worksheet.outline_settings(True, False, False, True)
731 worksheet.hide_gridlines(2)820 worksheet.hide_gridlines(2)
diff --git a/plainbox/impl/providers/exporters/units/exporter.pxu b/plainbox/impl/providers/exporters/units/exporter.pxu
index 65f96eb..c55843f 100644
--- a/plainbox/impl/providers/exporters/units/exporter.pxu
+++ b/plainbox/impl/providers/exporters/units/exporter.pxu
@@ -45,3 +45,10 @@ _summary: Generate junit XML
45entry_point: jinja245entry_point: jinja2
46file_extension: junit.xml46file_extension: junit.xml
47data: {"template": "junit.xml"}47data: {"template": "junit.xml"}
48
49unit: exporter
50id: tp-export
51_summary: Generate an Excel 2007+ XLSX export of a test plan
52entry_point: xlsx
53file_extension: xlsx
54options: tp-export
48\ No newline at end of file55\ No newline at end of file
diff --git a/plainbox/impl/runner.py b/plainbox/impl/runner.py
index 8adf4dc..7807a8f 100644
--- a/plainbox/impl/runner.py
+++ b/plainbox/impl/runner.py
@@ -518,6 +518,7 @@ class JobRunner(IJobRunner):
518 else:518 else:
519 result = self._just_run_command(519 result = self._just_run_command(
520 job, job_state, config).get_result()520 job, job_state, config).get_result()
521
521 return result522 return result
522523
523 def run_manual_job(self, job, job_state, config):524 def run_manual_job(self, job, job_state, config):
@@ -970,3 +971,34 @@ class JobRunner(IJobRunner):
970 logger.warning(971 logger.warning(
971 _("Please store desired files in $PLAINBOX_SESSION_SHARE and"972 _("Please store desired files in $PLAINBOX_SESSION_SHARE and"
972 " use regular temporary files for everything else"))973 " use regular temporary files for everything else"))
974
975
976class FakeJobRunner(JobRunner):
977
978 """
979 Fake runner for jobs.
980
981 Special runner that creates fake resource objects.
982 """
983
984 def run_resource_job(self, job, job_state, config):
985 """
986 Method called to run a job with plugin field equal to 'resource'.
987
988 Only one resouce object is created from this runner.
989 Exception: 'graphics_card' resource job creates two objects to
990 simulate hybrid graphics.
991 """
992 if job.plugin != "resource":
993 # TRANSLATORS: please keep 'plugin' untranslated
994 raise ValueError(_("bad job plugin value"))
995 builder = JobResultBuilder()
996 if job.partial_id == 'graphics_card':
997 builder.io_log = [(0, 'stdout', b'a: b\n'),
998 (1, 'stdout', b'\n'),
999 (2, 'stdout', b'a: c\n')]
1000 else:
1001 builder.io_log = [(0, 'stdout', b'a: b\n')]
1002 builder.outcome = 'pass'
1003 builder.return_code = 0
1004 return builder.get_result()
diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
index 9470905..995e38b 100644
--- a/plainbox/impl/session/assistant.py
+++ b/plainbox/impl/session/assistant.py
@@ -551,7 +551,7 @@ class SessionAssistant:
551 storage.remove()551 storage.remove()
552552
553 @raises(UnexpectedMethodCall)553 @raises(UnexpectedMethodCall)
554 def start_new_session(self, title: str):554 def start_new_session(self, title: str, runner_cls=JobRunner):
555 """555 """
556 Create a new testing session.556 Create a new testing session.
557557
@@ -588,7 +588,7 @@ class SessionAssistant:
588 self._metadata.flags = {'bootstrapping'}588 self._metadata.flags = {'bootstrapping'}
589 self._manager.checkpoint()589 self._manager.checkpoint()
590 self._command_io_delegate = JobRunnerUIDelegate(_SilentUI())590 self._command_io_delegate = JobRunnerUIDelegate(_SilentUI())
591 self._init_runner()591 self._init_runner(runner_cls)
592 self.session_available(self._manager.storage.id)592 self.session_available(self._manager.storage.id)
593 _logger.debug("New session created: %s", title)593 _logger.debug("New session created: %s", title)
594 UsageExpectation.of(self).allowed_calls = {594 UsageExpectation.of(self).allowed_calls = {
@@ -1541,7 +1541,8 @@ class SessionAssistant:
15411541
1542 @raises(KeyError, OSError)1542 @raises(KeyError, OSError)
1543 def export_to_file(1543 def export_to_file(
1544 self, exporter_id: str, option_list: 'list[str]', dir_path: str1544 self, exporter_id: str, option_list: 'list[str]', dir_path: str,
1545 filename: str=None
1545 ) -> str:1546 ) -> str:
1546 """1547 """
1547 Export the session to file using given exporter ID.1548 Export the session to file using given exporter ID.
@@ -1556,6 +1557,9 @@ class SessionAssistant:
1556 :param dir_path:1557 :param dir_path:
1557 Path to the directory where session file should be written to.1558 Path to the directory where session file should be written to.
1558 Note that the file name is automatically generated, based on1559 Note that the file name is automatically generated, based on
1560 :param filename:
1561 Optional file name (without extension)
1562 By default, the file name is automatically generated, based on
1559 creation time and type of exporter.1563 creation time and type of exporter.
1560 :returns:1564 :returns:
1561 Path to the written file.1565 Path to the written file.
@@ -1572,8 +1576,11 @@ class SessionAssistant:
1572 # issues when copying files.1576 # issues when copying files.
1573 isoformat = "%Y-%m-%dT%H.%M.%S.%f"1577 isoformat = "%Y-%m-%dT%H.%M.%S.%f"
1574 timestamp = datetime.datetime.utcnow().strftime(isoformat)1578 timestamp = datetime.datetime.utcnow().strftime(isoformat)
1579 basename = 'submission_' + timestamp
1580 if filename:
1581 basename = filename
1575 path = os.path.join(dir_path, ''.join(1582 path = os.path.join(dir_path, ''.join(
1576 ['submission_', timestamp, '.', exporter.unit.file_extension]))1583 [basename, '.', exporter.unit.file_extension]))
1577 with open(path, 'wb') as stream:1584 with open(path, 'wb') as stream:
1578 exporter.dump_from_session_manager(self._manager, stream)1585 exporter.dump_from_session_manager(self._manager, stream)
1579 return path1586 return path
@@ -1663,12 +1670,12 @@ class SessionAssistant:
1663 self.finish_bootstrap: "to finish bootstrapping",1670 self.finish_bootstrap: "to finish bootstrapping",
1664 }1671 }
16651672
1666 def _init_runner(self):1673 def _init_runner(self, runner_cls):
1667 self._execution_ctrl_list = []1674 self._execution_ctrl_list = []
1668 for ctrl_cls, args, kwargs in self._ctrl_setup_list:1675 for ctrl_cls, args, kwargs in self._ctrl_setup_list:
1669 self._execution_ctrl_list.append(1676 self._execution_ctrl_list.append(
1670 ctrl_cls(self._context.provider_list, *args, **kwargs))1677 ctrl_cls(self._context.provider_list, *args, **kwargs))
1671 self._runner = JobRunner(1678 self._runner = runner_cls(
1672 self._manager.storage.location,1679 self._manager.storage.location,
1673 self._context.provider_list,1680 self._context.provider_list,
1674 jobs_io_log_dir=os.path.join(1681 jobs_io_log_dir=os.path.join(
diff --git a/plainbox/impl/session/state.py b/plainbox/impl/session/state.py
index 9360f91..9dfc15a 100644
--- a/plainbox/impl/session/state.py
+++ b/plainbox/impl/session/state.py
@@ -799,6 +799,7 @@ class SessionState:
799 self._mandatory_job_list = []799 self._mandatory_job_list = []
800 self._run_list = []800 self._run_list = []
801 self._resource_map = {}801 self._resource_map = {}
802 self._fake_resources = False
802 self._metadata = SessionMetaData()803 self._metadata = SessionMetaData()
803 super(SessionState, self).__init__()804 super(SessionState, self).__init__()
804805
@@ -998,7 +999,8 @@ class SessionState:
998 any old entries), with a list of the resources that were parsed from999 any old entries), with a list of the resources that were parsed from
999 the IO log.1000 the IO log.
1000 """1001 """
1001 job.controller.observe_result(self, job, result)1002 job.controller.observe_result(
1003 self, job, result, fake_resources=self._fake_resources)
1002 self._recompute_job_readiness()1004 self._recompute_job_readiness()
10031005
1004 @deprecated('0.9', 'use the add_unit() method instead')1006 @deprecated('0.9', 'use the add_unit() method instead')
@@ -1100,7 +1102,7 @@ class SessionState:
1100 else:1102 else:
1101 # If there is a clash report DependencyDuplicateError only when the1103 # If there is a clash report DependencyDuplicateError only when the
1102 # hashes are different.1104 # hashes are different.
1103 if new_job != existing_job:1105 if new_job != existing_job and not self._fake_resources:
1104 raise DependencyDuplicateError(existing_job, new_job)1106 raise DependencyDuplicateError(existing_job, new_job)
1105 self._add_job_siblings_unit(new_job, recompute, via)1107 self._add_job_siblings_unit(new_job, recompute, via)
1106 return existing_job1108 return existing_job
diff --git a/plainbox/impl/unit/template.py b/plainbox/impl/unit/template.py
index 6e8a810..1103a76 100644
--- a/plainbox/impl/unit/template.py
+++ b/plainbox/impl/unit/template.py
@@ -139,6 +139,7 @@ class TemplateUnit(Unit):
139 super().__init__(139 super().__init__(
140 data, raw_data, origin, provider, parameters, field_offset_map)140 data, raw_data, origin, provider, parameters, field_offset_map)
141 self._filter_program = None141 self._filter_program = None
142 self._fake_resources = False
142143
143 @classmethod144 @classmethod
144 def instantiate_template(cls, data, raw_data, origin, provider, parameters,145 def instantiate_template(cls, data, raw_data, origin, provider, parameters,
@@ -302,7 +303,7 @@ class TemplateUnit(Unit):
302 all_units.load()303 all_units.load()
303 return all_units.get_by_name(self.template_unit).plugin_object304 return all_units.get_by_name(self.template_unit).plugin_object
304305
305 def instantiate_all(self, resource_list):306 def instantiate_all(self, resource_list, fake_resources=False):
306 """307 """
307 Instantiate a list of job definitions.308 Instantiate a list of job definitions.
308309
@@ -311,12 +312,15 @@ class TemplateUnit(Unit):
311 :param resource_list:312 :param resource_list:
312 A list of resource objects with the correct name313 A list of resource objects with the correct name
313 (:meth:`template_resource`)314 (:meth:`template_resource`)
315 :param fake_resources:
316 An optional parameter to trigger test plan export execution mode
314 :returns:317 :returns:
315 A list of new Unit (or subclass) objects.318 A list of new Unit (or subclass) objects.
316 """319 """
317 unit_cls = self.get_target_unit_cls()320 unit_cls = self.get_target_unit_cls()
318 resources = []321 resources = []
319 index = 0322 index = 0
323 self._fake_resources = fake_resources
320 for resource in resource_list:324 for resource in resource_list:
321 if self.should_instantiate(resource):325 if self.should_instantiate(resource):
322 index += 1326 index += 1
@@ -380,6 +384,13 @@ class TemplateUnit(Unit):
380 # See https://bugs.launchpad.net/bugs/1561821384 # See https://bugs.launchpad.net/bugs/1561821
381 parameters = {385 parameters = {
382 k: v for k, v in parameters.items() if k in accessed_parameters}386 k: v for k, v in parameters.items() if k in accessed_parameters}
387 if self._fake_resources:
388 parameters = {k: k.upper() for k in accessed_parameters}
389 for k in parameters:
390 if k.endswith('_slug'):
391 parameters[k] = k.replace('_slug', '').upper()
392 if 'index' in parameters:
393 parameters['index'] = index
383 # Add the special __index__ to the resource namespace variables394 # Add the special __index__ to the resource namespace variables
384 parameters['__index__'] = index395 parameters['__index__'] = index
385 # Instantiate the class using the instantiation API396 # Instantiate the class using the instantiation API
@@ -401,6 +412,8 @@ class TemplateUnit(Unit):
401 specified resource object would make the filter program evaluate to412 specified resource object would make the filter program evaluate to
402 True.413 True.
403 """414 """
415 if self._fake_resources:
416 return True
404 program = self.get_filter_program()417 program = self.get_filter_program()
405 if program is None:418 if program is None:
406 return True419 return True

Subscribers

People subscribed via source and target branches